/** * checkout-flows.spec.ts — Comprehensive checkout + auth flow tests. * * Covers: * - All three plan variants (free, professional, founding) * - Page structure, pricing, features list, noindex, canonical * - Auth section / payment section initial visibility per plan * - Form validation (empty fields, short password) * - Sign in / sign up toggle * - Mocked auth flows: sign-up success, email-confirm-required, * existing session, sign-in error * - DOM transitions: auth-section hidden → payment/free-success shown * - Auth badge rendered with user name after auth * - buyer-email pre-filled from Supabase user object * - /api/checkout endpoint response shapes * - /api/supabase-config CORS enforcement * - Edge cases: unknown plan, no plan param * * Network mocking strategy: Playwright route() intercepts * - GET /api/supabase-config → returns fake Supabase URL + anon key * - GET /auth/v1/user → no session or mock user * - POST /auth/v1/signup → success or email-confirm * - POST /auth/v1/token → sign-in success or error * This lets us test full JS-driven DOM transitions without real credentials. */ import { test, expect, type Page } from '@playwright/test'; // ─── Mock helpers ──────────────────────────────────────────────────────────── const FAKE_SUPA_URL = 'https://xyzfaketest.supabase.co'; const FAKE_ANON_KEY = 'fake-anon-key-for-playwright-testing'; const MOCK_USER = { id: 'test-uid-playwright-001', email: 'playwright@example.com', user_metadata: { full_name: 'Playwright Tester' }, }; const MOCK_SESSION = { access_token: 'fake-access-token-playwright', refresh_token: 'fake-refresh-token-playwright', token_type: 'bearer', expires_in: 3600, user: MOCK_USER, }; async function mockSupabaseConfig(page: Page) { await page.route('/api/supabase-config', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ url: FAKE_SUPA_URL, anon_key: FAKE_ANON_KEY }), }) ); } async function mockNoSession(page: Page) { await page.route(`${FAKE_SUPA_URL}/auth/v1/user`, (route) => route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'not_authenticated', message: 'JWT expired' }), }) ); } async function mockExistingSession(page: Page) { // Pre-seed localStorage with a fake Supabase session so getUser() fires // the /auth/v1/user HTTP request (Supabase v2 only calls the endpoint when // a stored token exists). Key format: sb-{projectRef}-auth-token. await page.addInitScript(([supaUrl, mockUser, mockSession]: [string, typeof MOCK_USER, typeof MOCK_SESSION]) => { const ref = new URL(supaUrl).hostname.split('.')[0]; // "xyzfaketest" const stored = { access_token: mockSession.access_token, token_type: 'bearer', expires_in: 3600, expires_at: Math.floor(Date.now() / 1000) + 3600, refresh_token: mockSession.refresh_token, user: mockUser, }; localStorage.setItem(`sb-${ref}-auth-token`, JSON.stringify(stored)); }, [FAKE_SUPA_URL, MOCK_USER, MOCK_SESSION] as [string, typeof MOCK_USER, typeof MOCK_SESSION]); // Mock the /auth/v1/user endpoint that Supabase calls to validate the token await page.route(`${FAKE_SUPA_URL}/auth/v1/user`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_USER), }) ); } async function mockSignUpSuccess(page: Page) { await page.route(`${FAKE_SUPA_URL}/auth/v1/signup`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { session: MOCK_SESSION, user: MOCK_USER }, error: null, ...MOCK_SESSION, }), }) ); } async function mockSignUpEmailConfirmRequired(page: Page) { await page.route(`${FAKE_SUPA_URL}/auth/v1/signup`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { session: null, user: MOCK_USER }, error: null, }), }) ); } async function mockSignInSuccess(page: Page) { await page.route(`${FAKE_SUPA_URL}/auth/v1/token*`, (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SESSION), }) ); } async function mockSignInError(page: Page, message = 'Invalid login credentials') { await page.route(`${FAKE_SUPA_URL}/auth/v1/token**`, (route) => route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ error: 'invalid_grant', error_description: message }), }) ); } // ─── Per-plan structure ─────────────────────────────────────────────────────── for (const plan of ['free', 'professional', 'founding'] as const) { test(`[${plan}] page loads 200 with content`, async ({ page }) => { const res = await page.goto(`/checkout?plan=${plan}`); expect(res?.status()).toBe(200); await expect(page.locator('body')).not.toBeEmpty(); }); test(`[${plan}] page has non-empty title`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); expect((await page.title()).trim().length).toBeGreaterThan(0); }); test(`[${plan}] nav back link to /`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('nav a[href="/"]').first()).toBeAttached(); }); test(`[${plan}] canonical is production URL — not stage/run.app`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); const canonical = await page.locator('link[rel="canonical"]').getAttribute('href'); expect(canonical).toContain('neurontechnologies.ai'); expect(canonical).not.toMatch(/run\.app|stage/); }); test(`[${plan}] noindex meta tag present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); const robots = await page.locator('meta[name="robots"]').getAttribute('content'); expect(robots).toContain('noindex'); }); test(`[${plan}] Google + GitHub social buttons present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#btn-google')).toBeAttached(); await expect(page.locator('#btn-github')).toBeAttached(); }); test(`[${plan}] email + password inputs present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#auth-email')).toBeAttached(); await expect(page.locator('#auth-password')).toBeAttached(); }); test(`[${plan}] auth message div present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#auth-message')).toBeAttached(); }); test(`[${plan}] auth badge container in DOM`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#auth-badge')).toBeAttached(); }); } // ─── Plan-specific content ──────────────────────────────────────────────────── test('[professional] shows $19 / month pricing', async ({ page }) => { await page.goto('/checkout?plan=professional'); const body = (await page.locator('body').textContent()) ?? ''; expect(body).toContain('$19'); expect(body.toLowerCase()).toContain('month'); }); test('[professional] features include persistent memory + API keys', async ({ page }) => { await page.goto('/checkout?plan=professional'); const body = (await page.locator('body').textContent()) ?? ''; expect(body).toContain('Persistent memory'); expect(body).toContain('Bring your own API keys'); }); test('[founding] shows $199 one-time pricing', async ({ page }) => { await page.goto('/checkout?plan=founding'); const body = (await page.locator('body').textContent()) ?? ''; expect(body).toContain('$199'); expect(body.toLowerCase()).toContain('one-time'); }); test('[founding] features include founding badge + lifetime', async ({ page }) => { await page.goto('/checkout?plan=founding'); const body = (await page.locator('body').textContent()) ?? ''; expect(body).toContain('Founding member badge'); expect(body.toLowerCase()).toContain('lifetime'); }); test('[free] shows free / no card pricing', async ({ page }) => { await page.goto('/checkout?plan=free'); const body = (await page.locator('body').textContent()) ?? ''; expect(body.toLowerCase()).toMatch(/\$0|free|no card/); }); test('[free] features include persistent memory + BYOAPI', async ({ page }) => { await page.goto('/checkout?plan=free'); const body = (await page.locator('body').textContent()) ?? ''; expect(body).toContain('Persistent memory'); }); // ─── Initial visibility per plan ───────────────────────────────────────────── test('[free] auth-section visible on load (account creation flow)', async ({ page }) => { await page.goto('/checkout?plan=free'); await expect(page.locator('#auth-section')).toBeVisible(); }); test('[free] payment-section hidden on load (shown after auth)', async ({ page }) => { await page.goto('/checkout?plan=free'); const ps = page.locator('#payment-section'); if (await ps.count() > 0) { await expect(ps).toBeHidden(); } }); test('[free] payment-element container present (Stripe mounts here)', async ({ page }) => { await page.goto('/checkout?plan=free'); await expect(page.locator('#payment-element')).toBeAttached(); }); test('[professional] payment-section visible on load', async ({ page }) => { await page.goto('/checkout?plan=professional'); await expect(page.locator('#payment-section')).toBeVisible(); }); test('[professional] auth-section hidden on load (optional for paid)', async ({ page }) => { await page.goto('/checkout?plan=professional'); await expect(page.locator('#auth-section')).toBeHidden(); }); test('[founding] payment-section visible on load', async ({ page }) => { await page.goto('/checkout?plan=founding'); await expect(page.locator('#payment-section')).toBeVisible(); }); test('[founding] auth-section hidden on load (optional for paid)', async ({ page }) => { await page.goto('/checkout?plan=founding'); await expect(page.locator('#auth-section')).toBeHidden(); }); // ─── Payment form elements (paid plans) ────────────────────────────────────── for (const plan of ['professional', 'founding'] as const) { test(`[${plan}] payment-element container present (Stripe mounts here)`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#payment-element')).toBeAttached(); }); test(`[${plan}] buyer-email input present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#buyer-email')).toBeAttached(); }); test(`[${plan}] submit/pay button present`, async ({ page }) => { await page.goto(`/checkout?plan=${plan}`); const submitBtn = page.locator('#submit-btn, .checkout-submit, button[type="submit"]').first(); await expect(submitBtn).toBeAttached(); }); } // ─── Form validation ────────────────────────────────────────────────────────── test('[free] submit with empty email shows auth error', async ({ page }) => { await mockSupabaseConfig(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.locator('.checkout-email-btn').click(); const msg = page.locator('#auth-message'); await expect(msg).toBeVisible({ timeout: 4000 }); const text = (await msg.textContent()) ?? ''; expect(text.toLowerCase()).toMatch(/email|password|enter|required/); }); test('[free] submit with password < 8 chars shows length error', async ({ page }) => { await mockSupabaseConfig(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.fill('#auth-email', 'test@example.com'); await page.fill('#auth-password', 'short'); await page.locator('.checkout-email-btn').click(); const msg = page.locator('#auth-message'); await expect(msg).toBeVisible({ timeout: 4000 }); const text = (await msg.textContent()) ?? ''; expect(text).toContain('8'); }); test('[free] submit with email only (no password) shows error', async ({ page }) => { await mockSupabaseConfig(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.fill('#auth-email', 'test@example.com'); // leave password empty await page.locator('.checkout-email-btn').click(); const msg = page.locator('#auth-message'); await expect(msg).toBeVisible({ timeout: 4000 }); }); // ─── Sign in / sign up toggle ───────────────────────────────────────────────── test('[free] initial button says "Create account"', async ({ page }) => { await page.goto('/checkout?plan=free'); await expect(page.locator('.checkout-email-btn')).toContainText('Create account'); }); test('[free] clicking "Sign in" link changes button text to "Sign in"', async ({ page }) => { await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.click('a[onclick*="showSignIn"]'); await expect(page.locator('.checkout-email-btn')).toContainText('Sign in'); }); test('[free] divider label changes for email mode', async ({ page }) => { await page.goto('/checkout?plan=free'); await expect(page.locator('#auth-divider-label')).toContainText(/email|account/i); }); // ─── Mocked free-plan auth flows ────────────────────────────────────────────── test('[free] successful sign-up → payment-section shown, auth-section hidden', async ({ page }) => { await mockSupabaseConfig(page); await mockSignUpSuccess(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.fill('#auth-email', 'newuser@example.com'); await page.fill('#auth-password', 'password123'); await page.locator('.checkout-email-btn').click(); await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 }); await expect(page.locator('#auth-section')).toBeHidden(); }); test('[free] sign-up email-confirm-required → shows check-email message', async ({ page }) => { await mockSupabaseConfig(page); await mockSignUpEmailConfirmRequired(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.fill('#auth-email', 'confirm@example.com'); await page.fill('#auth-password', 'password123'); await page.locator('.checkout-email-btn').click(); const msg = page.locator('#auth-message'); await expect(msg).toBeVisible({ timeout: 6000 }); const text = (await msg.textContent()) ?? ''; expect(text.toLowerCase()).toMatch(/email|confirm|check/); }); test('[free] sign-in success (via toggle) → payment-section shown', async ({ page }) => { await mockSupabaseConfig(page); await mockSignInSuccess(page); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.click('a[onclick*="showSignIn"]'); await page.fill('#auth-email', 'existing@example.com'); await page.fill('#auth-password', 'password123'); await page.locator('.checkout-email-btn').click(); await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 }); }); test('[free] sign-in error → shows error message, form stays visible', async ({ page }) => { await mockSupabaseConfig(page); await mockSignInError(page, 'Invalid login credentials'); await page.goto('/checkout?plan=free'); await page.waitForLoadState('domcontentloaded'); await page.click('a[onclick*="showSignIn"]'); await page.fill('#auth-email', 'wrong@example.com'); await page.fill('#auth-password', 'wrongpassword'); await page.locator('.checkout-email-btn').click(); const msg = page.locator('#auth-message'); await expect(msg).toBeVisible({ timeout: 6000 }); const text = (await msg.textContent()) ?? ''; expect(text.toLowerCase()).toMatch(/invalid|credential|incorrect|error/); }); // ─── Mocked paid-plan auth flows ───────────────────────────────────────────── for (const plan of ['professional', 'founding'] as const) { test(`[${plan}] existing session → auth badge visible with user info`, async ({ page }) => { await mockSupabaseConfig(page); await mockExistingSession(page); await page.goto(`/checkout?plan=${plan}`); const badge = page.locator('#auth-badge'); await expect(badge).toBeVisible({ timeout: 6000 }); const text = (await badge.textContent()) ?? ''; expect(text).toMatch(/Playwright Tester|playwright@example\.com/); }); test(`[${plan}] existing session → buyer-email pre-filled`, async ({ page }) => { await mockSupabaseConfig(page); await mockExistingSession(page); await page.goto(`/checkout?plan=${plan}`); await page.waitForFunction( () => { const el = document.getElementById('buyer-email') as HTMLInputElement | null; return el !== null && el.value.includes('@'); }, { timeout: 6000 } ); const val = await page.locator('#buyer-email').inputValue(); expect(val).toBe('playwright@example.com'); }); test(`[${plan}] existing session → auth-section hidden`, async ({ page }) => { await mockSupabaseConfig(page); await mockExistingSession(page); await page.goto(`/checkout?plan=${plan}`); // After session is detected auth-section stays/becomes hidden await page.waitForTimeout(2000); // let JS run await expect(page.locator('#auth-section')).toBeHidden(); }); test(`[${plan}] existing session → payment-section remains visible`, async ({ page }) => { await mockSupabaseConfig(page); await mockExistingSession(page); await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#payment-section')).toBeVisible({ timeout: 6000 }); }); test(`[${plan}] no session → payment form immediately visible`, async ({ page }) => { await mockSupabaseConfig(page); await mockNoSession(page); await page.goto(`/checkout?plan=${plan}`); await expect(page.locator('#payment-section')).toBeVisible({ timeout: 4000 }); await expect(page.locator('#payment-element')).toBeAttached(); }); } // ─── /api/payment-intent endpoint ──────────────────────────────────────────── test('POST /api/payment-intent free plan returns setup_mode (age verification)', async ({ request }) => { const res = await request.post('/api/payment-intent', { data: JSON.stringify({ plan: 'free', email: 'test@example.com' }), headers: { 'Content-Type': 'application/json' }, }); // Free plan creates a SetupIntent for age verification — must not 500 expect(res.status()).toBeLessThan(500); if (res.status() === 200) { const body = await res.json(); // Either setup_mode (success) or an error from Stripe (unconfigured env) — both valid expect('setup_mode' in body || 'client_secret' in body || 'error' in body).toBeTruthy(); // Must NOT return the old no_payment_required flag expect(body.no_payment_required).toBeFalsy(); } }); test('POST /api/payment-intent professional returns client_secret or config error (not 500)', async ({ request }) => { const res = await request.post('/api/payment-intent', { data: JSON.stringify({ plan: 'professional', email: 'test@example.com', name: 'Test User' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status()).toBeLessThan(500); if (res.status() === 200) { const body = await res.json(); expect('client_secret' in body || 'error' in body || 'setup_mode' in body).toBeTruthy(); } }); test('POST /api/payment-intent founding returns client_secret or config error (not 500)', async ({ request }) => { const res = await request.post('/api/payment-intent', { data: JSON.stringify({ plan: 'founding', email: 'test@example.com', name: 'Test User' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status()).toBeLessThan(500); }); test('POST /api/payment-intent empty body returns 4xx or config error (not 500)', async ({ request }) => { const res = await request.post('/api/payment-intent', { data: {} }); expect(res.status()).toBeLessThan(500); }); // ─── /api/supabase-config CORS ──────────────────────────────────────────────── test('GET /api/supabase-config with allowed origin returns url + anon_key', async ({ request }) => { const res = await request.get('/api/supabase-config', { headers: { Origin: 'https://neurontechnologies.ai' }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty('url'); expect(body).toHaveProperty('anon_key'); expect(body.url).toMatch(/supabase/); }); test('GET /api/supabase-config with disallowed origin returns 403', async ({ request }) => { const res = await request.get('/api/supabase-config', { headers: { Origin: 'https://evil-attacker.com' }, }); expect(res.status()).toBe(403); }); // ─── Edge cases ─────────────────────────────────────────────────────────────── test('[unknown plan] defaults gracefully — 200 and non-empty body', async ({ page }) => { const res = await page.goto('/checkout?plan=unknown'); expect(res?.status()).toBe(200); const body = (await page.locator('body').textContent()) ?? ''; expect(body.trim().length).toBeGreaterThan(100); }); test('[no plan param] checkout loads without error', async ({ page }) => { const res = await page.goto('/checkout'); expect(res?.status()).toBe(200); await expect(page.locator('body')).not.toBeEmpty(); }); test('[checkout] page has no JS console errors on load (professional)', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); page.on('pageerror', (err) => errors.push(err.message)); await page.goto('/checkout?plan=professional'); await page.waitForTimeout(2000); // Filter out known third-party noise (Stripe, Supabase unreachable in test env) const criticalErrors = errors.filter( (e) => !e.includes('stripe') && !e.includes('Stripe') && !e.includes('supabase') && !e.includes('Failed to fetch') && !e.includes('net::ERR') && !e.includes('Content Security Policy') ); expect(criticalErrors).toHaveLength(0); }); test('[checkout] page has no JS console errors on load (free)', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); page.on('pageerror', (err) => errors.push(err.message)); await page.goto('/checkout?plan=free'); await page.waitForTimeout(2000); const criticalErrors = errors.filter( (e) => !e.includes('stripe') && !e.includes('Stripe') && !e.includes('supabase') && !e.includes('Failed to fetch') && !e.includes('net::ERR') && !e.includes('Content Security Policy') ); expect(criticalErrors).toHaveLength(0); });