feat(homepage): expand inline lead form to name/email/phone/type/message
Ping Search Engines / Notify Search Engines (push) Successful in 3s
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Failing after 10m58s
Deploy to Production / Build & Verify (push) Failing after 16m37s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m3s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m3s
Deploy to Production / Deploy to Render (push) Failing after 14m55s
Deploy to Production / Deploy to Railway (push) Failing after 14m55s
Deploy to Production / Post-Deploy Verification (push) Failing after 13m32s
Deploy to Production / Notify on Failure (push) Failing after 13m51s

Rework the homepage InlineLeadForm from a single combined "email or phone"
contact field to an explicit 5-field layout — name, email (type=email),
phone (type=tel), project type and an optional message — so each field gets
the correct mobile keyboard and leads carry a real email + phone separately.

- Required fields kept to name + email + project type for a low-friction
  mobile flow; phone and message are optional.
- Email uses type=email + inputmode=email; phone uses type=tel + inputmode=tel
  for the numeric keypad. Message is an optional textarea; when blank the
  client synthesizes a >=10 char summary so the /api/contact validator passes.
- Still posts to the same /api/contact endpoint tagged source=homepage-inline,
  delivering leads exactly the same way the /contact page does (no page nav).
- Update inline-lead-form.spec.ts to the new field set and keyboards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
WorkRoot Agent
2026-06-17 20:33:26 +05:30
co-authored by Claude Opus 4.8
parent 870075b33f
commit 5a6db9c20f
3 changed files with 200 additions and 96 deletions
+163 -79
View File
@@ -1,20 +1,25 @@
--- ---
// InlineLeadForm — Three-field lead capture rendered inline on the homepage. // InlineLeadForm — Compact lead capture rendered inline on the homepage.
// //
// The goal is to cut the friction step of routing every CTA through /contact. // Goal: let visitors who reach the bottom of the homepage submit a project
// Visitors who reach the bottom of the homepage can submit name + contact // enquiry without the friction of navigating to /contact. The submission
// (email OR phone) + project type without a navigation. The submission reuses // reuses the SAME /api/contact endpoint as the full contact page and is tagged
// /api/contact under the hood and is tagged `source: 'homepage-inline'` so the // `source: 'homepage-inline'` so the backend / analytics layer can segment
// backend / analytics layer can segment these leads cleanly. // these leads cleanly (it also routes them to the `[Project Inquiry]` admin
// subject prefix).
// //
// Why a single "contact" field instead of separate email + phone: // Fields (kept to 5, mobile-first):
// - 3 fields keeps the form psychologically tiny and dramatically lifts // 1. Full name — required
// completion. Real-world conversion testing repeatedly shows 3 → 5+ fields // 2. Email — required, type="email" (email keyboard on mobile)
// is the biggest single-step drop-off on B2B lead forms. // 3. Phone — optional, type="tel" (numeric keypad on mobile)
// - We sniff the value on submit. If it has an "@" we treat it as email. // 4. Project type — required, <select> mapped to the backend `subject`
// Otherwise we treat it as a phone and synthesize a placeholder email to // 5. Message — optional <textarea>; synthesized when left blank so
// satisfy the backend's strict email validation. The phone is also sent // the backend's message>=10 char rule always passes
// through, so the admin email body shows the real reachable value. //
// Only name + email + project type are *required* — phone and message stay
// optional to keep completion friction low on mobile, which is where most of
// these submissions originate. The backend already treats `phone` as optional
// and only validates its format when present.
--- ---
<section <section
@@ -108,40 +113,68 @@
</p> </p>
</div> </div>
<!-- Contact (email or phone) --> <!-- Email + Phone row -->
<div> <div class="grid sm:grid-cols-2 gap-5">
<label <!-- Email (required) -->
for="inline-lead-contact" <div>
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5" <label
> for="inline-lead-email"
Email or phone <span class="text-red-500" aria-hidden="true">*</span> class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
<span class="sr-only">(required)</span> >
</label> Email <span class="text-red-500" aria-hidden="true">*</span>
<input <span class="sr-only">(required)</span>
type="text" </label>
id="inline-lead-contact" <input
name="contact" type="email"
required id="inline-lead-email"
autocomplete="email tel" name="email"
inputmode="email" required
aria-required="true" maxlength="254"
aria-describedby="inline-lead-contact-hint inline-lead-contact-error" autocomplete="email"
placeholder="jane@company.com or +91 95614 17403" inputmode="email"
class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors" aria-required="true"
/> aria-describedby="inline-lead-email-error"
<p placeholder="jane@company.com"
id="inline-lead-contact-hint" class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"
class="mt-1.5 text-xs text-secondary-500 dark:text-secondary-400" />
> <p
Drop your best email or phone number — whichever is easiest. id="inline-lead-email-error"
</p> class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
<p role="alert"
id="inline-lead-contact-error" >
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden" Please enter a valid email address.
role="alert" </p>
> </div>
Please enter a valid email address or phone number.
</p> <!-- Phone (optional) -->
<div>
<label
for="inline-lead-phone"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Phone
<span class="font-normal text-secondary-400 dark:text-secondary-500">(optional)</span>
</label>
<input
type="tel"
id="inline-lead-phone"
name="phone"
autocomplete="tel"
inputmode="tel"
pattern="[+()0-9.\s\-]{7,}"
maxlength="32"
aria-describedby="inline-lead-phone-error"
placeholder="+91 95614 17403"
class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"
/>
<p
id="inline-lead-phone-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please enter a valid phone number, or leave it blank.
</p>
</div>
</div> </div>
<!-- Project type --> <!-- Project type -->
@@ -184,6 +217,33 @@
</p> </p>
</div> </div>
<!-- Message (optional) -->
<div>
<label
for="inline-lead-message"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Project details
<span class="font-normal text-secondary-400 dark:text-secondary-500">(optional)</span>
</label>
<textarea
id="inline-lead-message"
name="message"
rows="3"
maxlength="5000"
aria-describedby="inline-lead-message-error"
placeholder="A sentence or two about what you want to build, your timeline, or budget…"
class="block w-full min-h-[96px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors resize-y"
></textarea>
<p
id="inline-lead-message-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please add a little more detail (at least 10 characters), or leave it blank.
</p>
</div>
<!-- General form error (server-side / network) --> <!-- General form error (server-side / network) -->
<div <div
id="inline-lead-form-error" id="inline-lead-form-error"
@@ -267,13 +327,17 @@
if (!form) return; if (!form) return;
var nameInput = document.getElementById('inline-lead-name'); var nameInput = document.getElementById('inline-lead-name');
var contactInput = document.getElementById('inline-lead-contact'); var emailInput = document.getElementById('inline-lead-email');
var phoneInput = document.getElementById('inline-lead-phone');
var projectTypeInput = document.getElementById('inline-lead-project-type'); var projectTypeInput = document.getElementById('inline-lead-project-type');
var messageInput = document.getElementById('inline-lead-message');
var websiteInput = document.getElementById('inline-lead-website'); var websiteInput = document.getElementById('inline-lead-website');
var nameError = document.getElementById('inline-lead-name-error'); var nameError = document.getElementById('inline-lead-name-error');
var contactError = document.getElementById('inline-lead-contact-error'); var emailError = document.getElementById('inline-lead-email-error');
var phoneError = document.getElementById('inline-lead-phone-error');
var projectTypeError = document.getElementById('inline-lead-project-type-error'); var projectTypeError = document.getElementById('inline-lead-project-type-error');
var messageError = document.getElementById('inline-lead-message-error');
var formError = document.getElementById('inline-lead-form-error'); var formError = document.getElementById('inline-lead-form-error');
var submitBtn = document.getElementById('inline-lead-submit'); var submitBtn = document.getElementById('inline-lead-submit');
@@ -283,7 +347,7 @@
var successPanel = document.getElementById('inline-lead-success'); var successPanel = document.getElementById('inline-lead-success');
// Tolerant phone test: allow +, digits, spaces, dashes, parens, dots. // Tolerant phone test: allow +, digits, spaces, dashes, parens, dots.
// We require at least 7 digits so single-digit / accidental presses fail. // Require at least 7 digits so single-digit / accidental presses fail.
var PHONE_REGEX = /^[+]?[()\d\s.\-]{7,}$/; var PHONE_REGEX = /^[+]?[()\d\s.\-]{7,}$/;
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -311,6 +375,10 @@
} }
} }
function phoneIsValid(val) {
return PHONE_REGEX.test(val) && digitsOnly(val).length >= 7;
}
function validate() { function validate() {
var ok = true; var ok = true;
@@ -322,14 +390,21 @@
showError(nameError, false); showError(nameError, false);
} }
var contactVal = (contactInput.value || '').trim(); var emailVal = (emailInput.value || '').trim();
var isEmail = EMAIL_REGEX.test(contactVal); if (!EMAIL_REGEX.test(emailVal)) {
var isPhone = PHONE_REGEX.test(contactVal) && digitsOnly(contactVal).length >= 7; showError(emailError, true);
if (!contactVal || (!isEmail && !isPhone)) {
showError(contactError, true);
ok = false; ok = false;
} else { } else {
showError(contactError, false); showError(emailError, false);
}
// Phone is optional — only validate format when something was typed.
var phoneVal = (phoneInput.value || '').trim();
if (phoneVal && !phoneIsValid(phoneVal)) {
showError(phoneError, true);
ok = false;
} else {
showError(phoneError, false);
} }
var projectVal = projectTypeInput.value || ''; var projectVal = projectTypeInput.value || '';
@@ -340,13 +415,24 @@
showError(projectTypeError, false); showError(projectTypeError, false);
} }
// Message is optional — only enforce the min length when something was typed.
var messageVal = (messageInput.value || '').trim();
if (messageVal && messageVal.length < 10) {
showError(messageError, true);
ok = false;
} else {
showError(messageError, false);
}
return ok; return ok;
} }
// Live-validate on input/change so the error UI doesn't linger after fix. // Live-validate on blur/change so the error UI clears once a field is fixed.
nameInput.addEventListener('blur', validate); nameInput.addEventListener('blur', validate);
contactInput.addEventListener('blur', validate); emailInput.addEventListener('blur', validate);
phoneInput.addEventListener('blur', validate);
projectTypeInput.addEventListener('change', validate); projectTypeInput.addEventListener('change', validate);
messageInput.addEventListener('blur', validate);
// Map the user-friendly project type label to the backend subject code so // Map the user-friendly project type label to the backend subject code so
// the existing /api/contact validator passes. Anything unmapped → 'other'. // the existing /api/contact validator passes. Anything unmapped → 'other'.
@@ -359,41 +445,39 @@
}; };
function buildPayload() { function buildPayload() {
var contactVal = (contactInput.value || '').trim();
var isEmail = EMAIL_REGEX.test(contactVal);
var nameVal = (nameInput.value || '').trim(); var nameVal = (nameInput.value || '').trim();
var emailVal = (emailInput.value || '').trim();
var phoneVal = (phoneInput.value || '').trim();
var projectType = projectTypeInput.value || 'Other'; var projectType = projectTypeInput.value || 'Other';
var subject = SUBJECT_BY_PROJECT_TYPE[projectType] || 'other'; var subject = SUBJECT_BY_PROJECT_TYPE[projectType] || 'other';
var messageVal = (messageInput.value || '').trim();
var email, phone; // The backend requires a message of at least 10 characters. When the
if (isEmail) { // visitor leaves the (optional) details blank, synthesize a concise
email = contactVal; // summary so the submission still goes through and the admin email
phone = undefined; // carries the useful context.
var message;
if (messageVal.length >= 10) {
message = messageVal;
} else { } else {
// Treat as phone. Synthesize a stable placeholder email so the message =
// backend's strict email validator passes — the admin notification 'New project enquiry from the homepage form.\n\n' +
// surfaces the real phone in both Phone: and Message: fields so the 'Project type: ' + projectType + '\n' +
// team always has a real way to reach the lead. 'Name: ' + nameVal +
var digits = digitsOnly(contactVal) || 'unknown'; (phoneVal ? '\nPhone: ' + phoneVal : '') +
email = 'lead-' + digits + '@homepage.workroot.in'; (messageVal ? '\n\nNote: ' + messageVal : '');
phone = contactVal;
} }
var message =
'New inline lead from the homepage.\n\n' +
'Project type: ' + projectType + '\n' +
'Preferred contact: ' + contactVal;
var payload = { var payload = {
name: nameVal, name: nameVal,
email: email, email: emailVal,
subject: subject, subject: subject,
message: message, message: message,
source: 'homepage-inline', source: 'homepage-inline',
projectType: projectType, projectType: projectType,
website: (websiteInput && websiteInput.value) || '', website: (websiteInput && websiteInput.value) || '',
}; };
if (phone) payload.phone = phone; if (phoneVal) payload.phone = phoneVal;
return payload; return payload;
} }
+5 -3
View File
@@ -744,9 +744,11 @@ const techStack = [
<!-- Inline Lead Form — sits between the tech-stack social-proof block <!-- Inline Lead Form — sits between the tech-stack social-proof block
and the final CTA band so visitors who are ready to start a project can and the final CTA band so visitors who are ready to start a project can
submit a 3-field enquiry without leaving the homepage. Submits to the submit a compact enquiry (name, email, phone, project type, message)
same /api/contact endpoint and is tagged `source: 'homepage-inline'` so without leaving the homepage. Required fields are kept to name + email +
the lead can be segmented downstream. --> project type for a low-friction mobile flow. Submits to the same
/api/contact endpoint and is tagged `source: 'homepage-inline'` so the
lead can be segmented downstream. -->
<InlineLeadForm /> <InlineLeadForm />
<!-- Homepage FAQ — single canonical FAQ section for /. Covers the cost / <!-- Homepage FAQ — single canonical FAQ section for /. Covers the cost /
+32 -14
View File
@@ -6,7 +6,10 @@ import { test, expect } from '@playwright/test';
* Covers: * Covers:
* - The form is mounted on the homepage between FAQ/tech-stack and the * - The form is mounted on the homepage between FAQ/tech-stack and the
* final CTA band. * final CTA band.
* - Filling all three fields and submitting calls /api/contact and shows * - It exposes the compact 5-field layout (name, email, phone, project type,
* message) with the correct mobile keyboard hints per field. Only name,
* email and project type are required; phone and message are optional.
* - Filling the required fields and submitting calls /api/contact and shows
* an in-place thank-you state without a full-page navigation. * an in-place thank-you state without a full-page navigation.
* - The submission is tagged `source: 'homepage-inline'` so leads can be * - The submission is tagged `source: 'homepage-inline'` so leads can be
* segmented downstream. * segmented downstream.
@@ -15,7 +18,7 @@ import { test, expect } from '@playwright/test';
*/ */
test.describe('Inline Lead Form (homepage)', () => { test.describe('Inline Lead Form (homepage)', () => {
test('form is visible on the homepage and has the three required fields', async ({ page }) => { test('form is visible on the homepage with the compact field set and mobile keyboards', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const form = page.getByTestId('inline-lead-form'); const form = page.getByTestId('inline-lead-form');
@@ -23,8 +26,10 @@ test.describe('Inline Lead Form (homepage)', () => {
await expect(form).toBeVisible(); await expect(form).toBeVisible();
await expect(page.locator('#inline-lead-name')).toBeVisible(); await expect(page.locator('#inline-lead-name')).toBeVisible();
await expect(page.locator('#inline-lead-contact')).toBeVisible(); await expect(page.locator('#inline-lead-email')).toBeVisible();
await expect(page.locator('#inline-lead-phone')).toBeVisible();
await expect(page.locator('#inline-lead-project-type')).toBeVisible(); await expect(page.locator('#inline-lead-project-type')).toBeVisible();
await expect(page.locator('#inline-lead-message')).toBeVisible();
// Touch-target sanity: the submit button must be at least 48px tall. // Touch-target sanity: the submit button must be at least 48px tall.
const submit = page.locator('#inline-lead-submit'); const submit = page.locator('#inline-lead-submit');
@@ -32,9 +37,17 @@ test.describe('Inline Lead Form (homepage)', () => {
expect(submitBox).not.toBeNull(); expect(submitBox).not.toBeNull();
expect(submitBox!.height).toBeGreaterThanOrEqual(48); expect(submitBox!.height).toBeGreaterThanOrEqual(48);
// Correct mobile-friendly attributes on the contact field. // Correct mobile keyboard per field.
await expect(page.locator('#inline-lead-contact')).toHaveAttribute('autocomplete', 'email tel'); await expect(page.locator('#inline-lead-email')).toHaveAttribute('type', 'email');
await expect(page.locator('#inline-lead-contact')).toHaveAttribute('inputmode', 'email'); await expect(page.locator('#inline-lead-email')).toHaveAttribute('inputmode', 'email');
await expect(page.locator('#inline-lead-email')).toHaveAttribute('autocomplete', 'email');
await expect(page.locator('#inline-lead-phone')).toHaveAttribute('type', 'tel');
await expect(page.locator('#inline-lead-phone')).toHaveAttribute('inputmode', 'tel');
await expect(page.locator('#inline-lead-phone')).toHaveAttribute('autocomplete', 'tel');
// Phone + message are optional (no `required` attribute).
await expect(page.locator('#inline-lead-phone')).not.toHaveAttribute('required', /.*/);
await expect(page.locator('#inline-lead-message')).not.toHaveAttribute('required', /.*/);
}); });
test('blocks submit when required fields are empty', async ({ page }) => { test('blocks submit when required fields are empty', async ({ page }) => {
@@ -57,12 +70,12 @@ test.describe('Inline Lead Form (homepage)', () => {
// The blank-field errors should surface and the network should NOT be hit. // 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-name-error')).toBeVisible();
await expect(page.locator('#inline-lead-contact-error')).toBeVisible(); await expect(page.locator('#inline-lead-email-error')).toBeVisible();
await expect(page.locator('#inline-lead-project-type-error')).toBeVisible(); await expect(page.locator('#inline-lead-project-type-error')).toBeVisible();
expect(apiCalled).toBe(false); expect(apiCalled).toBe(false);
}); });
test('fills and submits the form successfully with an email contact', async ({ page }) => { test('fills the required fields and submits successfully', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const form = page.getByTestId('inline-lead-form'); const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded(); await form.scrollIntoViewIfNeeded();
@@ -85,7 +98,7 @@ test.describe('Inline Lead Form (homepage)', () => {
}); });
await page.locator('#inline-lead-name').fill('Inline Lead Tester'); 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-email').fill('inline-lead@example.com');
await page.locator('#inline-lead-project-type').selectOption('Web App'); await page.locator('#inline-lead-project-type').selectOption('Web App');
await page.locator('#inline-lead-submit').click(); await page.locator('#inline-lead-submit').click();
@@ -103,11 +116,14 @@ test.describe('Inline Lead Form (homepage)', () => {
expect(captured.name).toBe('Inline Lead Tester'); expect(captured.name).toBe('Inline Lead Tester');
expect(captured.email).toBe('inline-lead@example.com'); expect(captured.email).toBe('inline-lead@example.com');
expect(captured.subject).toBe('web-development'); expect(captured.subject).toBe('web-development');
// Message is synthesized when left blank so the backend's >=10 char rule passes.
expect(typeof captured.message).toBe('string');
expect(captured.message.length).toBeGreaterThanOrEqual(10);
// Honeypot must be empty for a real user submission. // Honeypot must be empty for a real user submission.
expect(captured.website ?? '').toBe(''); expect(captured.website ?? '').toBe('');
}); });
test('routes a phone-shaped contact value into the phone field', async ({ page }) => { test('sends the phone and a typed message through their own fields', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const form = page.getByTestId('inline-lead-form'); const form = page.getByTestId('inline-lead-form');
await form.scrollIntoViewIfNeeded(); await form.scrollIntoViewIfNeeded();
@@ -127,17 +143,19 @@ test.describe('Inline Lead Form (homepage)', () => {
}); });
await page.locator('#inline-lead-name').fill('Phone Lead'); await page.locator('#inline-lead-name').fill('Phone Lead');
await page.locator('#inline-lead-contact').fill('+91 95614 17403'); await page.locator('#inline-lead-email').fill('phone-lead@example.com');
await page.locator('#inline-lead-phone').fill('+91 95614 17403');
await page.locator('#inline-lead-project-type').selectOption('Government / GeM'); await page.locator('#inline-lead-project-type').selectOption('Government / GeM');
await page.locator('#inline-lead-message').fill('We need a GeM-ready procurement portal.');
await page.locator('#inline-lead-submit').click(); await page.locator('#inline-lead-submit').click();
await expect(page.getByTestId('inline-lead-success')).toBeVisible(); await expect(page.getByTestId('inline-lead-success')).toBeVisible();
expect(captured).toBeTruthy(); expect(captured).toBeTruthy();
// The real email and phone are sent as-is in their own fields.
expect(captured.email).toBe('phone-lead@example.com');
expect(captured.phone).toBe('+91 95614 17403'); expect(captured.phone).toBe('+91 95614 17403');
// The client synthesizes a placeholder email so the backend's strict email expect(captured.message).toBe('We need a GeM-ready procurement portal.');
// 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.subject).toBe('government-systems');
expect(captured.source).toBe('homepage-inline'); expect(captured.source).toBe('homepage-inline');
}); });