Files
will.anderson cac7bd5727
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m52s
test: full Playwright + API test suite for stage
159 tests across three Playwright projects (api, chromium, mobile):
- tests/api/security.test.ts: security headers, CORS on /api/supabase-config
  (origin allowlist enforced), auth gate on /api/demo, Stripe webhook
  signature enforcement, source file leakage, path traversal, input
  validation (8000-char message cap)
- tests/api/endpoints.test.ts: /api/health, /api/founding-count shape
  invariants, /api/supabase-config JWT shape, sitemap.xml, robots.txt,
  /llms.txt, /api/soul-health internal gate, 404 for unknown routes
- tests/e2e/landing.spec.ts: title, h1 count, meta description, OG tags,
  canonical (no stage leak), JSON-LD schema, demo widget DOM presence,
  JS error filtering (known GTM/CSP noise excluded)
- tests/e2e/seo.spec.ts: per-page title patterns, noindex on checkout,
  canonical URLs, sitemap production-URL enforcement
- tests/e2e/checkout.spec.ts: all three plan variants, auth section, payment
  element, canonical
- tests/e2e/chat.spec.ts: widget DOM structure, auth gate (send button
  disabled without session), API-level auth rejection
- tests/e2e/navigation.spec.ts: all public routes return 200, 404s for
  removed/old paths (/terms, /enterprise-terms, /gallery), static files

All 159 pass against stage. CI step added to stage.yaml after smoke test.
2026-05-11 00:28:33 -05:00

81 lines
3.0 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Pages that must be indexed with production canonical URLs
const indexedPages = [
{ path: '/', titlePattern: /Neuron — The AI That Remembers You/ },
{ path: '/about', titlePattern: /About.*Neuron|Neuron.*About/i },
];
// Legal pages use /legal/ prefix
const legalPages = [
{ path: '/legal/terms', titlePattern: /Terms|Neuron/i },
{ path: '/legal/enterprise-terms', titlePattern: /Enterprise|Neuron/i },
];
for (const { path, titlePattern } of indexedPages) {
test(`${path} — title matches expected pattern`, async ({ page }) => {
await page.goto(path);
await expect(page).toHaveTitle(titlePattern);
});
test(`${path} — has meta description`, async ({ page }) => {
await page.goto(path);
const desc = await page.locator('meta[name="description"]').getAttribute('content');
expect(desc).toBeTruthy();
expect(desc!.length).toBeGreaterThan(30);
});
test(`${path} — canonical points to production domain, not stage`, async ({ page }) => {
await page.goto(path);
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href');
expect(canonical).toContain('neurontechnologies.ai');
expect(canonical).not.toContain('stage');
expect(canonical).not.toContain('run.app');
});
test(`${path} — has og:title`, async ({ page }) => {
await page.goto(path);
const ogTitle = await page.locator('meta[property="og:title"]').getAttribute('content');
expect(ogTitle).toBeTruthy();
expect(ogTitle!.length).toBeGreaterThan(5);
});
}
for (const { path, titlePattern } of legalPages) {
test(`${path} — renders with title`, async ({ page }) => {
const r = await page.goto(path);
expect(r?.status()).toBe(200);
await expect(page).toHaveTitle(titlePattern);
});
}
// Checkout must be noindex — it's a functional page, not content
test('Checkout page has noindex meta robots', async ({ page }) => {
await page.goto('/checkout?plan=professional');
const robots = page.locator('meta[name="robots"]');
await expect(robots).toHaveCount(1);
const content = await robots.getAttribute('content');
expect(content).toContain('noindex');
});
// Sitemap must only contain production URLs
test('Sitemap lists production URLs only (no stage or run.app)', async ({ page }) => {
const r = await page.request.get('/sitemap.xml');
expect(r.status()).toBe(200);
const text = await r.text();
expect(text).toContain('neurontechnologies.ai');
expect(text).not.toContain('run.app');
expect(text).not.toContain('stage');
expect(text).toContain('<urlset');
});
// The landing page must have JSON-LD structured data
test('Landing page has valid JSON-LD structured data', async ({ page }) => {
await page.goto('/');
const schemaContent = await page.locator('script[type="application/ld+json"]').textContent();
expect(schemaContent).toBeTruthy();
const parsed = JSON.parse(schemaContent!);
// Must be an object with @context at minimum
expect(parsed['@context']).toBeTruthy();
});