Files
CompanySite/tests/inline-lead-form.spec.ts
T
platform-mcp a4505635e1
E2E Test Suite / Form Interaction Tests (push) Failing after 10m19s
E2E Test Suite / Security Header Tests (push) Failing after 10m5s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m58s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m50s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m43s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m35s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m27s
Ping Search Engines / Notify Search Engines (push) Failing after 14m11s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m12s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Blocked by required conditions
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Blocked by required conditions
Deploy to Production / Deploy to Render (push) Blocked by required conditions
Deploy to Production / Deploy to VPS (PM2) (push) Blocked by required conditions
Deploy to Production / Deploy to Fly.io (push) Blocked by required conditions
Deploy to Production / Post-Deploy Verification (push) Blocked by required conditions
Deploy to Production / Notify on Failure (push) Has been cancelled
feat: add inline lead form to homepage
Mounts a 3-field lead-capture form (name, email-or-phone, project type)
between the FAQ/tech-stack block and the final CTA band so visitors who
are ready to start a project can submit without leaving the homepage.
Cuts the friction step of routing every CTA through /contact.

- New src/components/InlineLeadForm.astro:
  * Heading, micro-trust subhead, honeypot, client-side validation
  * Smart contact-field routing: detects email vs phone and synthesises
    a placeholder email for phone-only leads so the existing /api/contact
    validator accepts them (the real phone is preserved and surfaced in
    the admin notification)
  * In-place success state with phone + WhatsApp fallbacks; no navigation
  * 48px touch targets, dark-mode safe, autocomplete + inputmode attrs
- src/pages/api/contact.ts: accept and surface `source` / `projectType`
  metadata (allowlisted source values, free-text projectType capped at
  80 chars), add `erp-solutions` and `government-systems` subject codes,
  and include the source/project info in both the admin email and logs.
- src/pages/index.astro: import + mount InlineLeadForm above the final
  CTA section.
- tests/inline-lead-form.spec.ts: 8 specs covering UI render, mobile
  attributes, blank-field validation, email-submit + phone-submit
  payload routing, and server-side acceptance of the new payload shape,
  new subjects, and unknown-source fallback.
2026-06-17 11:49:47 +05:30

230 lines
8.8 KiB
TypeScript

import { test, expect } from '@playwright/test';
/**
* Inline Lead Form — Homepage
*
* Covers:
* - The form is mounted on the homepage between FAQ/tech-stack and the
* final CTA band.
* - Filling all three fields and submitting calls /api/contact and shows
* an in-place thank-you state without a full-page navigation.
* - The submission is tagged `source: 'homepage-inline'` so leads can be
* segmented downstream.
* - The backend itself accepts the new inline payload shape
* (server-side validation check).
*/
test.describe('Inline Lead Form (homepage)', () => {
test('form is visible on the homepage and has the three required fields', async ({ page }) => {
await page.goto('/');
const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded();
await expect(form).toBeVisible();
await expect(page.locator('#inline-lead-name')).toBeVisible();
await expect(page.locator('#inline-lead-contact')).toBeVisible();
await expect(page.locator('#inline-lead-project-type')).toBeVisible();
// Touch-target sanity: the submit button must be at least 48px tall.
const submit = page.locator('#inline-lead-submit');
const submitBox = await submit.boundingBox();
expect(submitBox).not.toBeNull();
expect(submitBox!.height).toBeGreaterThanOrEqual(48);
// Correct mobile-friendly attributes on the contact field.
await expect(page.locator('#inline-lead-contact')).toHaveAttribute('autocomplete', 'email tel');
await expect(page.locator('#inline-lead-contact')).toHaveAttribute('inputmode', 'email');
});
test('blocks submit when required fields are empty', async ({ page }) => {
await page.goto('/');
const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded();
// Intercept so a real submit can't slip through if the validator fails.
let apiCalled = false;
await page.route('**/api/contact', async (route) => {
apiCalled = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, message: 'ok' }),
});
});
await page.locator('#inline-lead-submit').click();
// The blank-field errors should surface and the network should NOT be hit.
await expect(page.locator('#inline-lead-name-error')).toBeVisible();
await expect(page.locator('#inline-lead-contact-error')).toBeVisible();
await expect(page.locator('#inline-lead-project-type-error')).toBeVisible();
expect(apiCalled).toBe(false);
});
test('fills and submits the form successfully with an email contact', async ({ page }) => {
await page.goto('/');
const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded();
// Capture the outgoing payload so we can assert source/projectType tagging.
let captured: any = null;
await page.route('**/api/contact', async (route) => {
const req = route.request();
try {
captured = req.postDataJSON();
} catch {
captured = null;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ success: true, message: 'Received.' }),
});
});
await page.locator('#inline-lead-name').fill('Inline Lead Tester');
await page.locator('#inline-lead-contact').fill('inline-lead@example.com');
await page.locator('#inline-lead-project-type').selectOption('Web App');
await page.locator('#inline-lead-submit').click();
// Success state should replace the form in-place, no navigation.
await expect(page.getByTestId('inline-lead-success')).toBeVisible();
await expect(page.getByTestId('inline-lead-success')).toContainText(
"Thanks! We'll reply within 24 hours."
);
await expect(page).toHaveURL(/\/$|\/#?$/);
// Confirm the payload is tagged correctly.
expect(captured).toBeTruthy();
expect(captured.source).toBe('homepage-inline');
expect(captured.projectType).toBe('Web App');
expect(captured.name).toBe('Inline Lead Tester');
expect(captured.email).toBe('inline-lead@example.com');
expect(captured.subject).toBe('web-development');
// Honeypot must be empty for a real user submission.
expect(captured.website ?? '').toBe('');
});
test('routes a phone-shaped contact value into the phone field', async ({ page }) => {
await page.goto('/');
const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded();
let captured: any = null;
await page.route('**/api/contact', async (route) => {
try {
captured = route.request().postDataJSON();
} catch {
captured = null;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, message: 'Received.' }),
});
});
await page.locator('#inline-lead-name').fill('Phone Lead');
await page.locator('#inline-lead-contact').fill('+91 95614 17403');
await page.locator('#inline-lead-project-type').selectOption('Government / GeM');
await page.locator('#inline-lead-submit').click();
await expect(page.getByTestId('inline-lead-success')).toBeVisible();
expect(captured).toBeTruthy();
expect(captured.phone).toBe('+91 95614 17403');
// The client synthesizes a placeholder email so the backend's strict email
// validator accepts phone-only contacts. The real phone is what matters.
expect(captured.email).toMatch(/^lead-\d+@homepage\.workroot\.in$/);
expect(captured.subject).toBe('government-systems');
expect(captured.source).toBe('homepage-inline');
});
});
// ============================================================
// Server-side: /api/contact accepts the inline lead payload
// ============================================================
test.describe('API: inline-lead payload (/api/contact)', () => {
test('accepts a payload tagged source=homepage-inline', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Inline API Test',
email: 'inline-api@example.com',
subject: 'web-development',
message:
'New inline lead from the homepage.\n\nProject type: Web App\nPreferred contact: inline-api@example.com',
source: 'homepage-inline',
projectType: 'Web App',
website: '',
},
});
// 200 success or 429 rate-limited (both acceptable in CI re-runs).
expect([200, 429]).toContain(response.status());
if (response.status() === 200) {
const body = (await response.json()) as { success: boolean };
expect(body.success).toBe(true);
}
});
test('accepts the new erp-solutions and government-systems subjects', async ({ request }) => {
for (const subject of ['erp-solutions', 'government-systems']) {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Inline API Subject Test',
email: `inline-${subject}@example.com`,
subject,
message: `Inline lead for ${subject}. Project type test.`,
source: 'homepage-inline',
projectType: subject === 'erp-solutions' ? 'ERP / Custom Software' : 'Government / GeM',
website: '',
},
});
// Either accepted or rate-limited — both prove the subject is recognized
// (a rejected subject would 422 instead).
expect([200, 429]).toContain(response.status());
}
});
test('still rejects payloads missing required fields', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: '',
email: 'inline-missing@example.com',
subject: 'web-development',
message: 'A message long enough to pass validation.',
source: 'homepage-inline',
website: '',
},
});
expect(response.status()).toBe(422);
const body = (await response.json()) as { success: boolean; error: string };
expect(body.success).toBe(false);
expect(body.error).toBeTruthy();
});
test('treats unknown source values as the safe default and still accepts the lead', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Unknown Source Test',
email: 'inline-unknown@example.com',
subject: 'other',
message: 'Inline lead from an unrecognised source string.',
source: 'totally-made-up-source',
projectType: 'Other',
website: '',
},
});
expect([200, 429]).toContain(response.status());
});
});