Files
neuron-web/tests/e2e/chat.spec.ts
T
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

76 lines
3.2 KiB
TypeScript

import { test, expect } from '@playwright/test';
// The demo widget is rendered server-side via El components and injected into
// the landing page. Element IDs are stable: #neuron-demo-panel, #neuron-demo-btn,
// #neuron-demo-auth, #neuron-demo-text, #neuron-demo-send, etc.
test.describe('Demo chat widget — structure', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
});
test('Demo panel (#neuron-demo-panel) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-panel')).toBeAttached();
});
test('Demo open button (#neuron-demo-btn) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-btn')).toBeAttached();
});
test('Demo auth section (#neuron-demo-auth) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-auth')).toBeAttached();
});
test('Demo text input (#neuron-demo-text) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-text')).toBeAttached();
});
test('Demo send button (#neuron-demo-send) is in the DOM', async ({ page }) => {
await expect(page.locator('#neuron-demo-send')).toBeAttached();
});
});
test.describe('Demo chat widget — auth gate', () => {
test.beforeEach(async ({ page }) => {
// Clear any stored Supabase session so we test the unauthenticated state
await page.goto('/');
await page.evaluate(() => {
Object.keys(localStorage)
.filter(k => k.startsWith('sb-') || k.includes('supabase'))
.forEach(k => localStorage.removeItem(k));
});
await page.reload();
await page.waitForLoadState('networkidle');
});
test('Send button is disabled when unauthenticated', async ({ page }) => {
const sendBtn = page.locator('#neuron-demo-send');
await expect(sendBtn).toBeAttached();
// The send button starts disabled until a valid session is confirmed
const isDisabled = await sendBtn.isDisabled().catch(() => true);
const isHidden = !(await sendBtn.isVisible().catch(() => false));
expect(isDisabled || isHidden).toBe(true);
});
test('Auth gate (#neuron-demo-auth) or gate (#neuron-demo-gate) is visible or panel is closed', async ({ page }) => {
// Either the auth pane is visible, OR the panel itself is closed (not visible).
// Both are correct unauthenticated states.
const authVisible = await page.locator('#neuron-demo-auth').isVisible().catch(() => false);
const gateVisible = await page.locator('#neuron-demo-gate').isVisible().catch(() => false);
const panelClosed = !(await page.locator('#neuron-demo-panel').isVisible().catch(() => true));
expect(authVisible || gateVisible || panelClosed).toBe(true);
});
});
test.describe('Demo chat widget — API gate (no browser session)', () => {
test('/api/demo rejects unauthenticated POST and returns auth_required', async ({ page }) => {
// Use the Playwright request context to hit the API directly
const r = await page.request.post('/api/demo', {
data: { message: 'Hello Neuron' },
});
const body = await r.json() as Record<string, unknown>;
expect(body.auth_required).toBe(true);
});
});