c6fd06b3de
- Block real Stripe CDN (js.stripe.com) in injectMockStripe() so the addInitScript mock is never overwritten by the async-loaded SDK - Replace waitForFunction(signUpWithEmail) with waitForLoadState in all 8 free-plan auth tests; defer scripts run before DOMContentLoaded so the function is guaranteed present without polling for it
594 lines
24 KiB
TypeScript
594 lines
24 KiB
TypeScript
/**
|
|
* 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 <fake-supabase>/auth/v1/user → no session or mock user
|
|
* - POST <fake-supabase>/auth/v1/signup → success or email-confirm
|
|
* - POST <fake-supabase>/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);
|
|
});
|