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
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:
co-authored by
Claude Opus 4.8
parent
870075b33f
commit
5a6db9c20f
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 /
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user