@evinvest/kitstart 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -12
- package/dist/core/accept.js +6 -2
- package/dist/core/analytics.d.ts +7 -2
- package/dist/core/lead-capture-text.d.ts +21 -1
- package/dist/core/lead-capture-text.js +16 -0
- package/dist/core/pricing/flow.d.ts +9 -0
- package/dist/core/pricing/flow.js +14 -0
- package/dist/core/pricing/index.d.ts +1 -1
- package/dist/core/pricing/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/LeadCapture.d.ts +42 -8
- package/dist/react/LeadCapture.js +174 -27
- package/dist/react/LeadCaptureChannels.d.ts +8 -2
- package/dist/react/LeadCaptureChannels.js +8 -2
- package/dist/react/LeadCaptureEstimate.d.ts +59 -26
- package/dist/react/LeadCaptureEstimate.js +81 -41
- package/dist/react/LeadCaptureFields.d.ts +2 -0
- package/dist/react/LeadCaptureFields.js +1 -1
- package/dist/react/LeadCaptureNeed.d.ts +27 -6
- package/dist/react/LeadCaptureNeed.js +10 -41
- package/dist/react/LeadCapturePrice.d.ts +36 -0
- package/dist/react/LeadCapturePrice.js +47 -0
- package/dist/react/LeadCaptureSteps.d.ts +47 -0
- package/dist/react/LeadCaptureSteps.js +39 -0
- package/dist/react/LeadCaptureTiles.d.ts +53 -0
- package/dist/react/LeadCaptureTiles.js +50 -0
- package/dist/react/QuoteFormShell.d.ts +5 -1
- package/dist/react/QuoteFormShell.js +2 -2
- package/dist/react/focus-next.d.ts +11 -0
- package/dist/react/focus-next.js +58 -0
- package/dist/react/index.d.ts +4 -1
- package/dist/react/lead-steps.d.ts +53 -0
- package/dist/react/lead-steps.js +58 -0
- package/dist/react/use-lead-context.d.ts +13 -0
- package/dist/react/use-lead-context.js +31 -0
- package/dist/react/use-lead-focus.d.ts +19 -0
- package/dist/react/use-lead-focus.js +70 -0
- package/dist/server/lead-webhook.d.ts +6 -9
- package/dist/server/lead-webhook.js +6 -8
- package/package.json +1 -1
- package/template/app/[locale]/[location]/prices/page.tsx +6 -1
- package/template/flake.nix +1 -1
- package/template/package.json +2 -2
- package/template/src/entities/content/model/en.ts +7 -0
- package/template/src/entities/content/model/fr.ts +7 -0
- package/template/src/entities/content/model/types.ts +8 -0
- package/template/src/shared/config/lead.ts +5 -1
- package/template/src/views/prices/index.ts +1 -0
- package/template/src/views/prices/ui/EstimateCard.tsx +66 -0
- package/template/tests/bundle_budget.txt +2 -1
- package/template/tests/e2e/steps.spec.ts +138 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// The prices page's estimate (`EstimateCard`): `LeadCapture` one question a
|
|
4
|
+
// screen. The intro first — urgent is a call back, the phone alone — then the
|
|
5
|
+
// need as cards, the regular clean's answers (the surface may be "I don't
|
|
6
|
+
// know", the frequency is cards that carry their price), the postcode, and
|
|
7
|
+
// the phone last under the price on one line.
|
|
8
|
+
|
|
9
|
+
const form = (page: Page) => page.locator("#estimate-form");
|
|
10
|
+
const screenOn = (page: Page) => form(page).locator("[data-lead-step]:not([data-lead-off])");
|
|
11
|
+
// The radio lies over its tile, as a finger meets it: a tap on the tile is a tap on the radio.
|
|
12
|
+
const tile = (page: Page, name: string) =>
|
|
13
|
+
screenOn(page)
|
|
14
|
+
.locator("label")
|
|
15
|
+
.filter({ has: page.getByText(name, { exact: true }) })
|
|
16
|
+
.locator("input[type=radio]");
|
|
17
|
+
const quote = (page: Page) => page.waitForResponse(r => r.request().method() === "POST" && new URL(r.url()).pathname === "/quote");
|
|
18
|
+
|
|
19
|
+
async function open(page: Page) {
|
|
20
|
+
// Monday 10:00 in Paris: open, so the folded callback is below the form, not leading it.
|
|
21
|
+
await page.clock.setFixedTime(new Date("2026-10-05T08:00:00Z"));
|
|
22
|
+
await page.goto("/fr/prices");
|
|
23
|
+
// Hydrated: the bar and the way back are the script's; without it the noscript style hides them.
|
|
24
|
+
await expect(form(page).locator("[data-lead-chrome]").first()).toBeVisible();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const height = (l: Locator) => l.evaluate(el => el.getBoundingClientRect().height);
|
|
28
|
+
|
|
29
|
+
test("one question a screen, the answers kept as chips, the price on one line with the tax credit, and booked", async ({ page }) => {
|
|
30
|
+
await open(page);
|
|
31
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "intro");
|
|
32
|
+
// The intro picks the branch, and so how many screens follow: no total said before it.
|
|
33
|
+
await expect(form(page).getByText("Étape 1", { exact: true })).toBeAttached();
|
|
34
|
+
// Touch-sized on a phone as on a desk.
|
|
35
|
+
expect(await height(tile(page, "Cette semaine").locator(".."))).toBeGreaterThanOrEqual(44);
|
|
36
|
+
expect(await height(form(page).getByRole("button", { name: "Retour" }).or(tile(page, "Je compare")).first())).toBeGreaterThanOrEqual(44);
|
|
37
|
+
await tile(page, "Cette semaine").click();
|
|
38
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "need");
|
|
39
|
+
// Cards, each with the brand's icon.
|
|
40
|
+
await expect(screenOn(page).locator("svg")).toHaveCount(4);
|
|
41
|
+
await tile(page, "Ménage régulier").click();
|
|
42
|
+
await tile(page, "2 chambres").click();
|
|
43
|
+
await tile(page, "40 à 70 m²").click();
|
|
44
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "estimate_frequency");
|
|
45
|
+
// 45 € + 30 € + 10 € = 85 €: less 15 % → 72 €, 10 % → 77 €, 5 % → 81 €.
|
|
46
|
+
const card = (name: string) => screenOn(page).locator("label").filter({ hasText: name });
|
|
47
|
+
await expect(card("Chaque semaine")).toContainText(/72\s€/);
|
|
48
|
+
await expect(card("Toutes les 2 semaines")).toContainText(/77\s€/);
|
|
49
|
+
await expect(card("Toutes les 2 semaines")).toContainText("Le plus avantageux");
|
|
50
|
+
await expect(card("Une fois")).toContainText(/85\s€/);
|
|
51
|
+
await tile(page, "Toutes les 2 semaines").click();
|
|
52
|
+
// The place serves one commune: the postcode is answered already, a chip — on to the phone.
|
|
53
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "phone");
|
|
54
|
+
await expect(form(page).getByLabel("Téléphone")).toBeFocused();
|
|
55
|
+
await expect(form(page).locator("[data-price-cents]")).toHaveText(/Votre prix : 77\s€/);
|
|
56
|
+
await expect(form(page).locator("[data-credit-cents]")).toContainText(/38,50\s€ après crédit d’impôt/);
|
|
57
|
+
await expect(screenOn(page).getByText("Votre numéro reste entre nous — rappel sous 15 min.")).toBeVisible();
|
|
58
|
+
// The answers, a tap back each; the way back one screen.
|
|
59
|
+
const chips = form(page).locator("[data-lead-chrome] li");
|
|
60
|
+
await expect(chips).toHaveCount(6);
|
|
61
|
+
await expect(chips.last()).toContainText("Paris");
|
|
62
|
+
await form(page).getByRole("button", { name: "Retour" }).click();
|
|
63
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "locality");
|
|
64
|
+
await expect(form(page).getByLabel("Code postal")).toHaveValue("Paris");
|
|
65
|
+
await form(page).getByLabel("Code postal").fill("75011");
|
|
66
|
+
await form(page).getByLabel("Code postal").press("Enter");
|
|
67
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "phone");
|
|
68
|
+
await expect(form(page).getByLabel("Téléphone")).toBeFocused();
|
|
69
|
+
await form(page).getByLabel("Téléphone").fill("0612345678");
|
|
70
|
+
const answered = quote(page);
|
|
71
|
+
await form(page).getByRole("button", { name: "Réserver" }).click();
|
|
72
|
+
const res = await answered;
|
|
73
|
+
const body = Object.fromEntries(new URLSearchParams(res.request().postData() ?? ""));
|
|
74
|
+
expect(body).toMatchObject({ urgency: "week", subject: "recurring", estimate_bedrooms: "t3", estimate_surface: "s70", estimate_frequency: "biweekly", locality: "75011", shown_cents: "7700" });
|
|
75
|
+
expect(await res.json()).toMatchObject({ ok: true, cents: 7700 });
|
|
76
|
+
await expect(page.getByRole("status")).toContainText(/Demande enregistrée au prix de 77\s€\./);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("\"Je ne sais pas\" makes the regular clean a quote: the questions after it go, no price, the thanks page", async ({ page }) => {
|
|
80
|
+
await open(page);
|
|
81
|
+
await tile(page, "Je compare").click();
|
|
82
|
+
await tile(page, "Ménage régulier").click();
|
|
83
|
+
await tile(page, "Studio").click();
|
|
84
|
+
await tile(page, "Je ne sais pas").click();
|
|
85
|
+
// The questions after it go; the commune the place serves is answered already.
|
|
86
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "phone");
|
|
87
|
+
await expect(form(page).getByText("Étape 6/6")).toBeAttached();
|
|
88
|
+
await expect(form(page).locator("[data-price-cents]")).toHaveCount(0);
|
|
89
|
+
await form(page).getByLabel("Téléphone").fill("0612345678");
|
|
90
|
+
const answered = quote(page);
|
|
91
|
+
await form(page).getByRole("button", { name: "Recevoir le prix" }).click();
|
|
92
|
+
const res = await answered;
|
|
93
|
+
const body = new URLSearchParams(res.request().postData() ?? "");
|
|
94
|
+
expect(body.get("estimate_surface")).toBe("?");
|
|
95
|
+
expect(body.has("estimate_frequency")).toBe(false);
|
|
96
|
+
expect(body.has("shown_cents")).toBe(false);
|
|
97
|
+
// A quote leaves for the thanks page; a priced lead would have stayed in the card.
|
|
98
|
+
await expect(page).toHaveURL(/\/fr\/thanks$/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("urgent is a call back: the phone and its consent, posted as a callback with the urgency", async ({ page }) => {
|
|
102
|
+
await open(page);
|
|
103
|
+
await tile(page, "Urgent — aujourd’hui").click();
|
|
104
|
+
await expect(screenOn(page)).toHaveAttribute("data-lead-step", "phone");
|
|
105
|
+
await expect(form(page).getByText("Étape 2/2")).toBeAttached();
|
|
106
|
+
await expect(page.locator("#estimate-callback")).toHaveCount(0);
|
|
107
|
+
await form(page).getByLabel("Téléphone").fill("0712345678");
|
|
108
|
+
await form(page).getByLabel("J’accepte d’être rappelé·e à ce numéro au sujet de ma demande.").check();
|
|
109
|
+
const answered = quote(page);
|
|
110
|
+
await form(page).getByRole("button", { name: "Être rappelé" }).click();
|
|
111
|
+
const body = new URLSearchParams((await answered).request().postData() ?? "");
|
|
112
|
+
expect(body.get("channel")).toBe("callback");
|
|
113
|
+
expect(body.get("urgency")).toBe("today");
|
|
114
|
+
await expect(page).toHaveURL(/\/fr\/thanks\?channel=callback$/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test.describe("without JavaScript", () => {
|
|
118
|
+
test.use({ javaScriptEnabled: false, reducedMotion: "reduce" });
|
|
119
|
+
|
|
120
|
+
test("every screen is there at once, and the form posts as one", async ({ page }) => {
|
|
121
|
+
await page.goto("/fr/prices");
|
|
122
|
+
await expect(form(page).locator("[data-lead-chrome]").first()).toBeHidden();
|
|
123
|
+
await expect(form(page).getByLabel("Téléphone")).toBeVisible();
|
|
124
|
+
await form(page).locator("label").filter({ has: page.getByText("Autre", { exact: true }) }).locator("input").check();
|
|
125
|
+
await form(page).getByLabel("Code postal").fill("75011");
|
|
126
|
+
await form(page).getByLabel("Téléphone").fill("0612345678");
|
|
127
|
+
const posted = page.waitForRequest(r => r.method() === "POST" && new URL(r.url()).pathname === "/quote");
|
|
128
|
+
await form(page).getByRole("button", { name: "Recevoir le prix" }).click();
|
|
129
|
+
expect(new URLSearchParams((await posted).postData() ?? "").get("subject")).toBe("other");
|
|
130
|
+
await expect(page).toHaveURL(/\/fr\/thanks$/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("the home page's form takes the postcode the link carries", async ({ page }) => {
|
|
135
|
+
await page.goto("/fr?postcode=75011");
|
|
136
|
+
await expect(page.locator("#quote select")).toHaveCount(0);
|
|
137
|
+
await expect(page.locator("#quote-form").getByLabel("Code postal")).toHaveValue("75011");
|
|
138
|
+
});
|