create-ailk 0.2.0 → 0.3.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/component-catalog.md +50 -53
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +2 -2
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +33 -5
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +459 -135
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +229 -4
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +14 -13
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +0 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/database/CHANGELOG.md +33 -0
- package/templates/database/inbox/schema.prisma +67 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1296
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/listing-promotion.ts +0 -342
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the results page's purchase hand-off composition (#4842).
|
|
3
|
-
*
|
|
4
|
-
* AC-1 — no PII request leaves before the visitor chooses: with the actions
|
|
5
|
-
* mocked and `globalThis.fetch` spied, rendering, opening "Why this?",
|
|
6
|
-
* selecting Get Started (the email step + price card appear), and an
|
|
7
|
-
* INVALID email submit all invoke nothing; only a valid email + Pay calls
|
|
8
|
-
* `startCheckoutAction` — call counts asserted at each of the four points.
|
|
9
|
-
* AC-2 — Pay calls `startCheckoutAction(slug, input)` once with the answers,
|
|
10
|
-
* email, locale, offerKey, the card's CURRENT seats / period / creditStop,
|
|
11
|
-
* and the permalink `payload` the action builds the two return URLs from
|
|
12
|
-
* (server-side, from the app's own origin — the #4875 security fix; the
|
|
13
|
-
* URLs themselves are asserted in `checkout.actions.test.ts`) — no
|
|
14
|
-
* `amount` / `total` / `unitAmount`.
|
|
15
|
-
* AC-3 — the configurable price card renders in the frame's SECOND slot
|
|
16
|
-
* (`flow-frame-active` — DESIGN §7.6's flip: fields left, price card
|
|
17
|
-
* right) at the D3 initial state showing `$249` (major units); stepping
|
|
18
|
-
* seats to 2
|
|
19
|
-
* re-renders `$498` with no request, and Pay then sends `seats: 2`; the
|
|
20
|
-
* three frame regions are the SAME DOM nodes before and after the
|
|
21
|
-
* transition; an offer with no seat-rate row — and every offer on a
|
|
22
|
-
* register with no `rateTable.currency` — gets Book a Call, never Get
|
|
23
|
-
* Started.
|
|
24
|
-
* AC-4 — "Email me this plan" + a valid email calls
|
|
25
|
-
* `saveRecommendationAction` once, renders the saved micro-state, never
|
|
26
|
-
* calls checkout, and leaves the safe-default side panel in place.
|
|
27
|
-
* AC-6 (the read half) — a `sessionStorage` entry rides as `sessionId` in
|
|
28
|
-
* both bodies; absent, the key is omitted entirely.
|
|
29
|
-
*
|
|
30
|
-
* Mocks: `../../../checkout.actions` (both actions — the boundary under
|
|
31
|
-
* test is ResultsClient → the actions, covered on their own by
|
|
32
|
-
* `checkout.actions.test.ts`), `next-intl` (identity translator), and a
|
|
33
|
-
* `globalThis.fetch` spy. `@working-theory/api-client` is mocked exactly as
|
|
34
|
-
* `page.test.tsx` does so an accidental import can never reach the network.
|
|
35
|
-
*/
|
|
36
|
-
import { render, screen, within } from "@testing-library/react";
|
|
37
|
-
import userEvent from "@testing-library/user-event";
|
|
38
|
-
import type { FlowConfig } from "@working-theory/validation";
|
|
39
|
-
import type { ReactElement } from "react";
|
|
40
|
-
|
|
41
|
-
const mockGet = jest.fn();
|
|
42
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
43
|
-
__esModule: true,
|
|
44
|
-
flows: { get: (...args: unknown[]) => mockGet(...args) },
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
const mockStart = jest.fn();
|
|
48
|
-
const mockSave = jest.fn();
|
|
49
|
-
// The boundary under test is ResultsClient's own wiring to its two Server
|
|
50
|
-
// Actions, not the actions themselves (checkout.actions.test.ts) — the same
|
|
51
|
-
// narrow-scope justification FlowStepperClient.permalink.test.tsx carries.
|
|
52
|
-
// eslint-disable-next-line no-restricted-syntax -- see comment above
|
|
53
|
-
jest.mock("../../../checkout.actions", () => ({
|
|
54
|
-
startCheckoutAction: (...args: unknown[]) => mockStart(...args),
|
|
55
|
-
saveRecommendationAction: (...args: unknown[]) => mockSave(...args),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
jest.mock("next-intl", () => ({
|
|
59
|
-
__esModule: true,
|
|
60
|
-
useTranslations: () => (key: string) => key,
|
|
61
|
-
useMessages: () => ({}),
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
import { ResultsClient } from "../ResultsClient";
|
|
65
|
-
|
|
66
|
-
const SLUG = "ailk-recommender";
|
|
67
|
-
const PAYLOAD = "v1-abc123";
|
|
68
|
-
const ANSWERS = { team_size: "agency", wants_seo: "yes" };
|
|
69
|
-
const STORAGE_KEY = `ailk:flow-session:${SLUG}`;
|
|
70
|
-
|
|
71
|
-
function fixtureConfig(overrides: Partial<FlowConfig> = {}): FlowConfig {
|
|
72
|
-
return {
|
|
73
|
-
slug: SLUG,
|
|
74
|
-
defaultLocale: "en",
|
|
75
|
-
mode: "recommending",
|
|
76
|
-
steps: [
|
|
77
|
-
{
|
|
78
|
-
key: "team",
|
|
79
|
-
type: "segmentation",
|
|
80
|
-
copy: { title: { en: "Team" } },
|
|
81
|
-
questions: [
|
|
82
|
-
{
|
|
83
|
-
name: "team_size",
|
|
84
|
-
options: [
|
|
85
|
-
{ value: "solo", label: { en: "Solo" } },
|
|
86
|
-
{ value: "agency", label: { en: "Agency" } },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: "wants_seo",
|
|
91
|
-
options: [
|
|
92
|
-
{ value: "yes", label: { en: "Yes" } },
|
|
93
|
-
{ value: "no", label: { en: "No" } },
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
offers: [
|
|
100
|
-
{
|
|
101
|
-
key: "pro",
|
|
102
|
-
copy: { name: { en: "Pro" }, description: { en: "For teams." } },
|
|
103
|
-
price: { kind: "exact", amount: 249, period: "annual" },
|
|
104
|
-
rules: [
|
|
105
|
-
{ when: { answer: "team_size", eq: "agency" }, reason: { en: "You run an agency." } },
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
key: "consult",
|
|
110
|
-
copy: { name: { en: "Consulting" } },
|
|
111
|
-
price: { kind: "label", label: { en: "Application-based" } },
|
|
112
|
-
rules: [
|
|
113
|
-
{ when: { answer: "wants_seo", eq: "yes" }, reason: { en: "You want organic reach." } },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
key: "free",
|
|
118
|
-
copy: { name: { en: "Free" } },
|
|
119
|
-
price: { kind: "label", label: { en: "Free" } },
|
|
120
|
-
rules: [{ when: { answer: "team_size", eq: "solo" }, reason: { en: "Solo." } }],
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
pairings: [
|
|
124
|
-
{
|
|
125
|
-
offers: ["pro", "consult"],
|
|
126
|
-
reason: { en: "Pro plus a consult covers both." },
|
|
127
|
-
when: { answer: "wants_seo", eq: "yes" },
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
rateTable: {
|
|
131
|
-
currency: "usd",
|
|
132
|
-
seatRates: [
|
|
133
|
-
{ offer: "pro", amount: 249, period: "annual" },
|
|
134
|
-
{ offer: "pro", amount: 25, period: "monthly" },
|
|
135
|
-
],
|
|
136
|
-
creditLadder: {
|
|
137
|
-
denomination: "annual",
|
|
138
|
-
tiers: [
|
|
139
|
-
{ credits: 10, amount: 99 },
|
|
140
|
-
{ credits: 50, amount: 399 },
|
|
141
|
-
],
|
|
142
|
-
custom: { en: "Contact us" },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
recommendation: { key: "recommendation", copy: { title: { en: "Your recommendation" } } },
|
|
146
|
-
...overrides,
|
|
147
|
-
} as FlowConfig;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderResults(
|
|
151
|
-
config: FlowConfig = fixtureConfig(),
|
|
152
|
-
extra: { returnState?: "success" | "cancelled" } = {},
|
|
153
|
-
): ReactElement {
|
|
154
|
-
const element = (
|
|
155
|
-
<ResultsClient
|
|
156
|
-
config={config}
|
|
157
|
-
locale="en"
|
|
158
|
-
answers={ANSWERS}
|
|
159
|
-
slug={SLUG}
|
|
160
|
-
payload={PAYLOAD}
|
|
161
|
-
{...extra}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
render(element);
|
|
165
|
-
return element;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const fetchSpy = jest.fn();
|
|
169
|
-
|
|
170
|
-
function expectNothingSent(): void {
|
|
171
|
-
expect(mockStart).not.toHaveBeenCalled();
|
|
172
|
-
expect(mockSave).not.toHaveBeenCalled();
|
|
173
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
174
|
-
expect(mockGet).not.toHaveBeenCalled();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const EXPECTED_INITIAL_BODY = {
|
|
178
|
-
answers: ANSWERS,
|
|
179
|
-
email: "jane@example.com",
|
|
180
|
-
locale: "en",
|
|
181
|
-
offerKey: "pro",
|
|
182
|
-
seats: 1,
|
|
183
|
-
period: "annual",
|
|
184
|
-
creditStop: { kind: "none" },
|
|
185
|
-
payload: PAYLOAD,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* What a redirecting Server Action's `await` surfaces to the client on the
|
|
190
|
-
* SUCCESS path — Next's `NEXT_REDIRECT` control-flow error, recognised by its
|
|
191
|
-
* digest (`redirect-error.js`: `NEXT_REDIRECT;<push|replace>;<url>;<status>;`).
|
|
192
|
-
*/
|
|
193
|
-
function nextRedirectError(url: string): Error {
|
|
194
|
-
const err = new Error("NEXT_REDIRECT");
|
|
195
|
-
(err as Error & { digest: string }).digest = `NEXT_REDIRECT;push;${url};307;`;
|
|
196
|
-
return err;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
beforeAll(() => {
|
|
200
|
-
Object.defineProperty(globalThis, "fetch", {
|
|
201
|
-
value: fetchSpy,
|
|
202
|
-
configurable: true,
|
|
203
|
-
writable: true,
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
beforeEach(() => {
|
|
208
|
-
mockGet.mockReset();
|
|
209
|
-
mockStart.mockReset();
|
|
210
|
-
mockSave.mockReset();
|
|
211
|
-
fetchSpy.mockReset();
|
|
212
|
-
window.sessionStorage.clear();
|
|
213
|
-
// A redirecting action never resolves with a value on success (Next
|
|
214
|
-
// navigates) — the default mock mirrors that: it resolves `undefined`.
|
|
215
|
-
mockStart.mockResolvedValue(undefined);
|
|
216
|
-
mockSave.mockResolvedValue({ ok: true, leadId: "lead_1" });
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
async function submitEmail(
|
|
220
|
-
user: ReturnType<typeof userEvent.setup>,
|
|
221
|
-
email: string,
|
|
222
|
-
via: "enter" | "pay" | "save" = "enter",
|
|
223
|
-
): Promise<void> {
|
|
224
|
-
const input = screen.getByLabelText(
|
|
225
|
-
via === "save" ? "emailStepSaveTitle" : "emailStepTitle",
|
|
226
|
-
);
|
|
227
|
-
await user.clear(input);
|
|
228
|
-
await user.type(input, email);
|
|
229
|
-
if (via === "enter") {
|
|
230
|
-
await user.keyboard("{Enter}");
|
|
231
|
-
} else {
|
|
232
|
-
await user.click(
|
|
233
|
-
screen.getByTestId(via === "pay" ? "flow-results-pay" : "flow-results-save"),
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
describe("AC-1 — no PII request leaves before the visitor chooses", () => {
|
|
239
|
-
it("invokes neither action nor fetch until a valid email + Pay", async () => {
|
|
240
|
-
const user = userEvent.setup();
|
|
241
|
-
renderResults();
|
|
242
|
-
|
|
243
|
-
// Point 1 — the cold render.
|
|
244
|
-
expect(screen.getByTestId("flow-results")).toBeInTheDocument();
|
|
245
|
-
expectNothingSent();
|
|
246
|
-
|
|
247
|
-
// Point 2 — opening a "Why this?" disclosure.
|
|
248
|
-
await user.click(screen.getByTestId("recommendation-card-pro-why-trigger"));
|
|
249
|
-
expect(screen.getByText("You run an agency.")).toBeInTheDocument();
|
|
250
|
-
expectNothingSent();
|
|
251
|
-
|
|
252
|
-
// Point 3 — Get Started: the email step + the price card appear.
|
|
253
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
254
|
-
expect(screen.getByTestId("results-email-step")).toBeInTheDocument();
|
|
255
|
-
expect(screen.getByTestId("checkout-price-card")).toBeInTheDocument();
|
|
256
|
-
expectNothingSent();
|
|
257
|
-
|
|
258
|
-
// Point 4 — an invalid email shows the validation message, sends nothing.
|
|
259
|
-
await submitEmail(user, "not-an-email");
|
|
260
|
-
expect(screen.getByRole("alert")).toHaveTextContent("validationInvalidEmail");
|
|
261
|
-
expectNothingSent();
|
|
262
|
-
|
|
263
|
-
// Only a valid email + Pay (Enter submits — #4806 FR4) calls the action.
|
|
264
|
-
await submitEmail(user, "jane@example.com");
|
|
265
|
-
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
266
|
-
expect(mockSave).not.toHaveBeenCalled();
|
|
267
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
describe("AC-2 — Pay calls checkout once, with no amount", () => {
|
|
272
|
-
it("sends exactly the answers, email, locale, offerKey, the card's state and the permalink payload", async () => {
|
|
273
|
-
const user = userEvent.setup();
|
|
274
|
-
renderResults();
|
|
275
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
276
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
277
|
-
|
|
278
|
-
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
279
|
-
expect(mockStart).toHaveBeenCalledWith(SLUG, EXPECTED_INITIAL_BODY);
|
|
280
|
-
const body = mockStart.mock.calls[0]?.[1] as Record<string, unknown>;
|
|
281
|
-
expect(body).toEqual(EXPECTED_INITIAL_BODY);
|
|
282
|
-
for (const key of ["amount", "total", "unitAmount", "successUrl", "cancelUrl"]) {
|
|
283
|
-
expect(Object.keys(body)).not.toContain(key);
|
|
284
|
-
}
|
|
285
|
-
expect(body).not.toHaveProperty("sessionId");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("never converts a NEXT_REDIRECT (the success path) into the error micro-state — Pay + Back stay pending", async () => {
|
|
289
|
-
const redirectErr = nextRedirectError("https://checkout.stripe.com/c/pay/cs_test_123");
|
|
290
|
-
mockStart.mockImplementation(() => {
|
|
291
|
-
throw redirectErr;
|
|
292
|
-
});
|
|
293
|
-
const user = userEvent.setup();
|
|
294
|
-
renderResults();
|
|
295
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
296
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
297
|
-
// Let the settled handler flush.
|
|
298
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
299
|
-
|
|
300
|
-
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
301
|
-
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
302
|
-
expect(screen.getByTestId("flow-results-pay")).toBeDisabled();
|
|
303
|
-
expect(screen.getByRole("button", { name: "back" })).toBeDisabled();
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("a NON-redirect throw from the action IS the error micro-state", async () => {
|
|
307
|
-
mockStart.mockImplementation(() => {
|
|
308
|
-
throw new TypeError("fetch failed");
|
|
309
|
-
});
|
|
310
|
-
const user = userEvent.setup();
|
|
311
|
-
renderResults();
|
|
312
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
313
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
314
|
-
expect(await screen.findByRole("alert")).toHaveTextContent("checkoutError");
|
|
315
|
-
expect(screen.getByTestId("flow-results-pay")).not.toBeDisabled();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("renders the checkout error and re-enables Pay when the action returns a failure", async () => {
|
|
319
|
-
mockStart.mockResolvedValue({ ok: false, kind: "not_configured" });
|
|
320
|
-
const user = userEvent.setup();
|
|
321
|
-
renderResults();
|
|
322
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
323
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
324
|
-
expect(await screen.findByRole("alert")).toHaveTextContent("checkoutError");
|
|
325
|
-
expect(screen.getByTestId("flow-results-pay")).not.toBeDisabled();
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
describe("AC-3 — the configurable price card mirrors price() in major units; unquotable offers get Book a Call", () => {
|
|
330
|
-
it("shows $249 at the D3 initial state, $498 after one seat step with no request, and Pay sends seats: 2", async () => {
|
|
331
|
-
const user = userEvent.setup();
|
|
332
|
-
renderResults();
|
|
333
|
-
|
|
334
|
-
const side = screen.getByTestId("flow-frame-side");
|
|
335
|
-
const activeRegion = screen.getByTestId("flow-frame-active");
|
|
336
|
-
const bar = screen.getByTestId("flow-frame-bar");
|
|
337
|
-
expect(within(side).getByTestId("flow-side-panel-safe-default")).toBeInTheDocument();
|
|
338
|
-
|
|
339
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
340
|
-
|
|
341
|
-
// The same three DOM nodes — the frame is never remounted (#4806 FR7).
|
|
342
|
-
expect(screen.getByTestId("flow-frame-side")).toBe(side);
|
|
343
|
-
expect(screen.getByTestId("flow-frame-active")).toBe(activeRegion);
|
|
344
|
-
expect(screen.getByTestId("flow-frame-bar")).toBe(bar);
|
|
345
|
-
|
|
346
|
-
// DESIGN §7.6 — the flip: the email step takes the FIRST slot (left at
|
|
347
|
-
// md+, first in the mobile stack); the price card takes the SECOND.
|
|
348
|
-
expect(within(side).getByTestId("results-email-step")).toBeInTheDocument();
|
|
349
|
-
expect(within(side).queryByTestId("flow-side-panel-safe-default")).not.toBeInTheDocument();
|
|
350
|
-
const card = within(activeRegion).getByTestId("checkout-price-card");
|
|
351
|
-
expect(within(card).getByTestId("checkout-price-card-offer-name")).toHaveTextContent("Pro");
|
|
352
|
-
expect(within(card).getByTestId("checkout-price-card-total")).toHaveTextContent("$249");
|
|
353
|
-
expect(within(card).queryByText("$2.49")).not.toBeInTheDocument();
|
|
354
|
-
expect(within(bar).getByTestId("flow-results-pay")).toBeInTheDocument();
|
|
355
|
-
|
|
356
|
-
await user.click(within(card).getByTestId("checkout-price-card-seats-increment"));
|
|
357
|
-
expect(within(card).getByTestId("checkout-price-card-total")).toHaveTextContent("$498");
|
|
358
|
-
expectNothingSent();
|
|
359
|
-
|
|
360
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
361
|
-
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
362
|
-
expect(mockStart.mock.calls[0]?.[1]).toEqual({
|
|
363
|
-
...EXPECTED_INITIAL_BODY,
|
|
364
|
-
seats: 2,
|
|
365
|
-
period: "annual",
|
|
366
|
-
creditStop: { kind: "none" },
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("Back returns to the results view with the recommendation intact and nothing sent", async () => {
|
|
371
|
-
const user = userEvent.setup();
|
|
372
|
-
renderResults();
|
|
373
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
374
|
-
await user.click(screen.getByRole("button", { name: "back" }));
|
|
375
|
-
expect(screen.getByTestId("flow-results")).toBeInTheDocument();
|
|
376
|
-
expect(screen.getByTestId("flow-side-panel-safe-default")).toBeInTheDocument();
|
|
377
|
-
expect(screen.queryByTestId("checkout-price-card")).not.toBeInTheDocument();
|
|
378
|
-
expectNothingSent();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("an offer with no seat-rate row renders Book a Call, never Get Started", () => {
|
|
382
|
-
renderResults();
|
|
383
|
-
expect(screen.getByTestId("recommendation-card-consult-terminal-bookCall")).toBeInTheDocument();
|
|
384
|
-
expect(
|
|
385
|
-
screen.queryByTestId("recommendation-card-consult-terminal-getStarted"),
|
|
386
|
-
).not.toBeInTheDocument();
|
|
387
|
-
expect(screen.getByTestId("recommendation-card-pro-terminal-getStarted")).toBeInTheDocument();
|
|
388
|
-
expect(screen.getByTestId("recommendation-pairing")).toBeInTheDocument();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("every offer on a register with no rateTable.currency renders Book a Call", () => {
|
|
392
|
-
const config = fixtureConfig();
|
|
393
|
-
const { currency: _currency, ...rateTable } = config.rateTable!;
|
|
394
|
-
renderResults({ ...config, rateTable });
|
|
395
|
-
expect(screen.getByTestId("recommendation-card-pro-terminal-bookCall")).toBeInTheDocument();
|
|
396
|
-
expect(screen.getByTestId("recommendation-card-consult-terminal-bookCall")).toBeInTheDocument();
|
|
397
|
-
expect(screen.queryAllByTestId(/-terminal-getStarted$/)).toHaveLength(0);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe("AC-4 — Email me this plan", () => {
|
|
402
|
-
it("calls saveRecommendationAction once, renders the saved state, never calls checkout, keeps the safe-default panel", async () => {
|
|
403
|
-
const user = userEvent.setup();
|
|
404
|
-
renderResults();
|
|
405
|
-
await user.click(screen.getByRole("button", { name: "emailMePlan" }));
|
|
406
|
-
expect(screen.getByTestId("flow-side-panel-safe-default")).toBeInTheDocument();
|
|
407
|
-
expect(screen.queryByTestId("checkout-price-card")).not.toBeInTheDocument();
|
|
408
|
-
expectNothingSent();
|
|
409
|
-
|
|
410
|
-
await submitEmail(user, "jane@example.com", "save");
|
|
411
|
-
|
|
412
|
-
expect(mockSave).toHaveBeenCalledTimes(1);
|
|
413
|
-
expect(mockSave).toHaveBeenCalledWith(SLUG, {
|
|
414
|
-
answers: ANSWERS,
|
|
415
|
-
email: "jane@example.com",
|
|
416
|
-
locale: "en",
|
|
417
|
-
});
|
|
418
|
-
expect(await screen.findByTestId("results-email-step-saved")).toBeInTheDocument();
|
|
419
|
-
expect(mockStart).not.toHaveBeenCalled();
|
|
420
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
421
|
-
expect(screen.getByTestId("flow-side-panel-safe-default")).toBeInTheDocument();
|
|
422
|
-
expect(screen.queryByTestId("checkout-price-card")).not.toBeInTheDocument();
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
describe("AC-6 (read half) — session resume is forwarded, never fabricated", () => {
|
|
427
|
-
it("forwards a stored session id in both bodies", async () => {
|
|
428
|
-
window.sessionStorage.setItem(STORAGE_KEY, "fs_walk_1");
|
|
429
|
-
const user = userEvent.setup();
|
|
430
|
-
renderResults();
|
|
431
|
-
|
|
432
|
-
await user.click(screen.getByRole("button", { name: "emailMePlan" }));
|
|
433
|
-
await submitEmail(user, "jane@example.com", "save");
|
|
434
|
-
expect(mockSave.mock.calls[0]?.[1]).toMatchObject({ sessionId: "fs_walk_1" });
|
|
435
|
-
|
|
436
|
-
await user.click(screen.getByRole("button", { name: "back" }));
|
|
437
|
-
await user.click(screen.getByTestId("recommendation-card-pro-terminal-getStarted"));
|
|
438
|
-
await submitEmail(user, "jane@example.com", "pay");
|
|
439
|
-
expect(mockStart.mock.calls[0]?.[1]).toMatchObject({ sessionId: "fs_walk_1" });
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("omits the key entirely when no entry exists", async () => {
|
|
443
|
-
const user = userEvent.setup();
|
|
444
|
-
renderResults();
|
|
445
|
-
await user.click(screen.getByRole("button", { name: "emailMePlan" }));
|
|
446
|
-
await submitEmail(user, "jane@example.com", "save");
|
|
447
|
-
expect(mockSave.mock.calls[0]?.[1]).not.toHaveProperty("sessionId");
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe("D6 — the return micro-state renders over the same recommendation", () => {
|
|
452
|
-
it("renders the status line for success and nothing for an unrecognised value", () => {
|
|
453
|
-
renderResults(fixtureConfig(), { returnState: "success" });
|
|
454
|
-
expect(screen.getByTestId("flow-results-return-state")).toHaveTextContent("checkoutSuccess");
|
|
455
|
-
expect(screen.getByTestId("recommendation-card-pro")).toBeInTheDocument();
|
|
456
|
-
});
|
|
457
|
-
});
|