Epic: #2553 — Forms: Embed → hosted partner subdomain flow
Scope: Phase 0 — core handoff at app.leadtrap.ai/forms/continue?handoff=...
Shipped in: #2631 (closes #2550)
Environment: dev.leadtrap.ai (or production after deploy confirmation)
Flow under test: embed intake → server-driven OpenPageStep redirect → /forms/continue?handoff=… → behavioral wizard
- Test partner is in the
internalTestingFeaturevisor segment (hostedFormRedirectandformsSetupTabare 0% for*today) - Campaign has:
- At least one intake form with questions
- At least one enabled behavioral form with questions (required for redirect to fire)
Both must be enabled for the test partner (via internalTesting segment):
-
hostedFormRedirect— server-side gate inresolveNextStep -
formsSetupTab— optional; provides embed code from Setup tab
There is no admin UI for hosted redirect yet — configure via GraphQL:
mutation {
updateWebFormProductConfiguration(input: {
id: "<configuration-id>"
hostedRedirect: {
enabled: true
target: TOP # also test SELF and BLANK separately
delayMs: null # also test 2000 for delay case
}
}) {
id
hostedRedirect { enabled target delayMs previewUrl }
}
}-
hostedRedirect.enabled = true - Confirm
previewUrlishttps://<FRONTEND_HOSTNAME>/forms/continue
- Embed snippet from Forms → Setup tab (or existing embed page)
- Test page loads the iframe embed (
/platform/frame/forms?partner_id=…)
| # | Step | Expected |
|---|---|---|
| 1 | Open partner embed page | Intake form loads with branding |
| 2 | Complete all intake questions | Submit succeeds |
| 3 | Observe redirect | Browser navigates to …/forms/continue?handoff=<code> (top window if TOP) |
| 4 | Hosted page loads | No admin chrome; full-page form (not iframe) |
| 5 | Behavioral wizard | Opens directly on behavioral questions (not intake gate) |
| 6 | Intake answers hydrated | Previously submitted intake values are present in session/state |
| 7 | Complete behavioral form | Submit succeeds; lead updated |
| 8 | Same visitorSessionId |
Backend lead/session ties embed + hosted legs together |
Admin verification:
- Lead appears with intake + behavioral answers
- Activity/timeline looks correct
Run the happy path three times with different hostedRedirect.target:
| Target | Expected navigation |
|---|---|
TOP |
window.top.location → hosted URL (breaks out of iframe) |
SELF |
iframe navigates to hosted URL |
BLANK |
new tab opens hosted URL (noopener,noreferrer) |
- With
delayMs: 2000, redirect waits ~2s before navigating
| # | Scenario | Expected |
|---|---|---|
| 1 | Visit /forms/continue with no ?handoff= |
“This form link is invalid.” |
| 2 | Visit with garbage handoff code | “invalid or has expired” |
| 3 | Redeem same handoff URL twice (refresh/back) | Second load fails — code is single-use (Redis GETDEL) |
| 4 | Wait >2 min, then redeem code | Expired — code TTL is 120s |
| 5 | hostedFormRedirect flag off for partner |
Intake shows thank-you screen, no redirect |
| 6 | hostedRedirect.enabled = false |
Thank-you, no redirect |
| 7 | No behavioral forms on config | Thank-you, no redirect |
| 8 | Intake incomplete (skip required field) | No redirect; validation blocks submit |
| 9 | Disqualified lead (service area / qualifier) | Qualifier result screen, no redirect |
| 10 | Prior embed session in same tab, then new handoff | Hosted page shows correct handoff data (no stale Zustand state) |
- Handoff code is opaque hex, not guessable (24 random bytes)
-
continueFormHandoffis public but returnsnullfor bad codes (no error leakage) - Replay of used code returns null, not previous session
- Hosted page loads config via
partnerIdfrom redeemed session (not URL param)
| Check | Dev | Prod |
|---|---|---|
/forms/continue?handoff=… serves hosted-forms.html |
[ ] | [ ] |
/forms/continue/ (trailing slash) works |
[ ] | [ ] |
/forms/continue does not fall through to admin SPA |
[ ] | [ ] |
With flags at 0% for general traffic:
- Existing embed-only partners unaffected — intake → thank-you, no redirect
- No
OpenPageStepin GraphQL response when redirect preconditions fail
Already covered by unit/integration tests:
-
HostedFormsAppJest tests (missing/expired/success) -
continueFormHandofffunctional + unit tests -
executeSubmitNextStepredirect URL builder tests -
formHandoffCodeManagersingle-use + TTL tests
Manual sections above remain the Phase 0 sign-off gate until e2e is added (see below).
| Area | Tester | Date | Pass? |
|---|---|---|---|
| Happy path (TOP) | |||
| SELF / BLANK targets | |||
| Single-use + expiry | |||
| Flag/config gating | |||
| Prod routing |
Yes — a Playwright e2e test is possible, but it needs new fixtures; nothing in e2e/ covers forms today.
| Manual case | E2E approach |
|---|---|
| Happy path (TOP) | Parent HTML page embeds iframe → fill intake → assert top-level URL matches /forms/continue?handoff= → fill behavioral question |
| Invalid / missing handoff | Direct page.goto('/forms/continue') + assert error text |
| Single-use replay | Capture URL after redirect → goto twice → second load shows expired |
| Flag/config off | Seed partner without redirect preconditions → assert thank-you, no navigation |
| SELF / BLANK | Separate tests with different hostedRedirect.target in seed data |
| Case | Why |
|---|---|
| Code TTL (120s) | Waiting 2+ minutes in e2e is slow/flaky; already covered by form_handoff_code_manager tests |
resolveNextStep preconditions |
Faster and more reliable as web_form_product_answer_manager unit/functional tests |
Defense-in-depth on continueFormHandoff |
Resolver unit tests already exist |
-
Test seed — extend
POST /api/test/create-test-data(or add/api/test/create-forms-handoff-fixture) to create:- Partner in
internalTestingsegment (or bypass flags in test env) - Web form config with intake + behavioral forms
hostedRedirect.enabled = true
- Partner in
-
Fixture page —
frontend/public/tests/e2e/forms-embed.htmlmirroringdefault.htmlbut loading the forms iframe embed -
Playwright spec —
e2e/src/tests/forms-hosted-handoff.spec.ts:- Helper to fill Magic Forms fields by label
- Intercept or read
handofffrom URL after redirect - Optional: GraphQL poll to verify lead answers post-behavioral submit
-
CI — add
npm run test:forms-handoffscript; wire intoe2e.yml(full stack required)
forms-hosted-handoff.spec.ts
✓ redirects to hosted continue page after intake submit (TOP)
✓ shows behavioral question on hosted page
✓ rejects missing handoff param
✓ rejects replayed handoff code
Target variants (SELF, BLANK, delayMs) can be separate tests once the seed API accepts config overrides.
~1–2 days for MVP happy path + negative URL cases, assuming test seed API work. Full parity with this manual plan would be ~3–4 days.
After Phase 0 passes: epic stays open for #2552 (partner subdomains), #2625 (remove embed “next step” display), and #2618 (handoff test polish).