diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 18e1c7c32..15398f3f5 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -73,8 +73,8 @@ test('pricing page routes software and support CTAs without checkout', async ({ await page.goto('/pricing'); await expect(page.getByRole('link', { name: 'Install from npm' })).toHaveAttribute('href', /npmjs\.com\/package\/@threadplane\/chat/); - await expect(page.getByRole('link', { name: 'Discuss assurance' })).toHaveAttribute('href', '/contact?source=pricing_production_assurance'); - await expect(page.getByRole('link', { name: 'Talk to Sales' })).toHaveAttribute('href', '/contact?source=pricing_tier_enterprise'); + await expect(page.getByRole('link', { name: 'Discuss assurance' })).toHaveAttribute('href', '/contact?intent=enterprise&entry=pricing_tier_production_assurance'); + await expect(page.getByRole('link', { name: 'Talk to Sales' })).toHaveAttribute('href', '/contact?intent=enterprise&entry=pricing_tier_enterprise'); await expect(page.locator('form[action*="checkout"]')).toHaveCount(0); }); @@ -87,35 +87,29 @@ test('pricing page is responsive without page-level horizontal overflow', async } }); -test('pricing page lead form validates required fields', async ({ page }) => { +test('pricing plan buttons lead to the enterprise contact intent', async ({ page }) => { await page.goto('/pricing'); - const leadForm = page.locator('#lead-form form'); - await expect(leadForm).toBeVisible(); - await leadForm.getByRole('button', { name: 'Request enterprise quote' }).click(); - await expect(leadForm).toBeVisible(); - expect(await leadForm.evaluate((form) => (form as HTMLFormElement).checkValidity())).toBe(false); + await expect(page.getByRole('link', { name: 'Talk to Sales' })).toHaveAttribute('href', '/contact?intent=enterprise&entry=pricing_tier_enterprise'); + await expect(page.getByRole('link', { name: 'Request a conversation' })).toHaveAttribute('href', '/contact?intent=enterprise&entry=pricing_enterprise_band'); + await expect(page.locator('#lead-form')).toHaveCount(0); }); test('contact page submits a lead payload and renders success state', async ({ page }) => { let leadPayload: Record | undefined; await page.route('**/api/leads', async (route) => { leadPayload = route.request().postDataJSON(); - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ ok: true }), - }); + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) }); }); await page.goto('/contact'); - const contactForm = page.locator('main form').first(); - await contactForm.getByRole('textbox', { name: 'Email', exact: true }).fill('jane@acme.com'); - await contactForm.getByRole('textbox', { name: 'Name' }).fill('Jane Smith'); - await contactForm.getByRole('textbox', { name: 'Company' }).fill('Acme'); - await contactForm.getByRole('textbox', { name: 'Message' }).fill('We are evaluating Threadplane.'); - await contactForm.getByRole('button', { name: 'Send' }).click(); - - await expect(page.getByText("Thanks. We'll be in touch within one business day.")).toBeVisible(); + const form = page.locator('main form').first(); + await form.getByLabel('Work email').fill('jane@acme.com'); + await form.getByLabel('Name').fill('Jane Smith'); + await form.getByLabel('Company').fill('Acme'); + await form.getByLabel('What are you shipping?').fill('We are evaluating Threadplane.'); + await form.getByRole('button', { name: 'Send to Brian' }).click(); + + await expect(page.getByRole('status')).toContainText('Sent.'); expect(leadPayload).toMatchObject({ form_kind: 'contact', email: 'jane@acme.com', @@ -127,35 +121,33 @@ test('contact page submits a lead payload and renders success state', async ({ p expect(leadPayload?.['submission_id']).toMatch(UUID_V4); }); -test('pricing lead form posts to /api/leads and renders success state', async ({ page }) => { +test('contact page enterprise intent posts the pricing form kind with a timeline', async ({ page }) => { let leadPayload: Record | undefined; await page.route('**/api/leads', async (route) => { leadPayload = route.request().postDataJSON(); - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ ok: true }), - }); + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) }); }); - await page.goto('/pricing#lead-form'); - const leadForm = page.locator('#lead-form form'); - await leadForm.getByLabel('Name').fill('Jane Smith'); - await leadForm.getByLabel('Work email').fill('jane@acme.com'); - await leadForm.getByLabel('Company').fill('Acme'); - await leadForm.getByLabel('Tell us about your use case').fill('Volume seats and security review.'); - await leadForm.getByRole('button', { name: 'Request enterprise quote' }).click(); + await page.goto('/contact?intent=enterprise&entry=pricing_tier_enterprise'); + const form = page.locator('main form').first(); + await form.getByRole('button', { name: 'Request a conversation' }).click(); + await expect(form.getByText('Enter your email address.')).toBeVisible(); - await expect(page.getByText(/we'll be in touch within one business day/i)).toBeVisible(); + await form.getByLabel('Work email').fill('jane@acme.com'); + await form.getByLabel('Company').fill('Acme'); + await form.getByLabel('Timeline').selectOption('this_quarter'); + await form.getByLabel('Tell us about your use case').fill('Volume seats and security review.'); + await form.getByRole('button', { name: 'Request a conversation' }).click(); + + await expect(page.getByRole('status')).toContainText('Sent.'); expect(leadPayload).toMatchObject({ form_kind: 'pricing', email: 'jane@acme.com', - name: 'Jane Smith', company: 'Acme', + timeline: 'this_quarter', message: 'Volume seats and security review.', policy_version: GROWTH_FORM_POLICY_VERSION, }); - expect(leadPayload?.['submission_id']).toMatch(UUID_V4); }); test('footer newsletter form posts to /api/newsletter and renders success state', async ({ page }) => { diff --git a/apps/website/src/app/contact/page.spec.tsx b/apps/website/src/app/contact/page.spec.tsx new file mode 100644 index 000000000..6a35de8f3 --- /dev/null +++ b/apps/website/src/app/contact/page.spec.tsx @@ -0,0 +1,66 @@ +// @vitest-environment jsdom +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; + +const track = vi.hoisted(() => vi.fn()); + +vi.mock('../../lib/analytics/client', () => ({ track, trackCtaClick: vi.fn(), trackExternalLinkClick: vi.fn() })); +vi.mock('server-only', () => ({})); +vi.mock('../../components/contact/GitHubStarsPill', () => ({ GitHubStarsPill: () => })); + +import ContactPage from './page'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('ContactPage', () => { + it('renders the contact variant by default', async () => { + render(await ContactPage({ searchParams: Promise.resolve({}) })); + expect(screen.getByText('Contact', { selector: '[data-ui="eyebrow"]' })).toBeTruthy(); + expect(screen.getByRole('heading', { level: 1 }).textContent).toBe('Talk to an engineer.'); + expect(screen.getByRole('button', { name: 'Send to Brian' })).toBeTruthy(); + expect(screen.queryByLabelText('Timeline')).toBeNull(); + const email = screen.getByRole('link', { name: 'brian@threadplane.ai' }); + expect(email.getAttribute('href')).toBe('mailto:brian@threadplane.ai'); + const github = screen.getByRole('link', { name: 'GitHub issues' }); + expect(github.getAttribute('href')).toBe('https://github.com/cacheplane/angular-agent-framework/issues'); + const discord = screen.getByRole('link', { name: 'Discord' }); + expect(discord.getAttribute('href')).toBe('https://discord.gg/cacheplane'); + expect(screen.getByTestId('stars')).toBeTruthy(); + }); + + it('renders the enterprise variant from the intent query and passes the entry point through', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200 })); + render(await ContactPage({ searchParams: Promise.resolve({ intent: 'enterprise', entry: 'pricing_tier_enterprise' }) })); + expect(screen.getByText('Enterprise', { selector: '[data-ui="eyebrow"]' })).toBeTruthy(); + expect(screen.getByLabelText('Timeline')).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Request a conversation' })).toBeTruthy(); + + fireEvent.change(screen.getByLabelText('Work email'), { target: { value: 'buyer@example.com' } }); + fireEvent.change(screen.getByLabelText('Company'), { target: { value: 'Acme Co' } }); + fireEvent.change(screen.getByLabelText('Timeline'), { target: { value: 'this_quarter' } }); + fireEvent.click(screen.getByRole('button', { name: 'Request a conversation' })); + + expect(track).toHaveBeenCalledWith( + 'marketing:lead_form_submit', + expect.objectContaining({ surface: 'pricing', entry_point: 'pricing_tier_enterprise' }) + ); + }); + + it('ignores unknown intents and unsafe entry values', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200 })); + render(await ContactPage({ searchParams: Promise.resolve({ intent: 'weird', entry: '