create-ailk 0.20.0 → 0.22.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 +21 -1
- package/component-catalog.json +220 -8
- package/component-catalog.md +21 -9
- package/dist/add-module.js +9 -5
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +63 -25
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +30 -14
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +124 -1
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +125 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +20 -7
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +139 -3
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +24 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +386 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +504 -24
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +12 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +184 -41
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +120 -16
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +249 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -19
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +192 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +40 -12
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/database/CHANGELOG.md +47 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/seed.test.ts +72 -2
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +60 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
|
@@ -70,7 +70,7 @@ describe("courses/page — snapshot regression", () => {
|
|
|
70
70
|
});
|
|
71
71
|
const { container } = render(element);
|
|
72
72
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
73
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Courses","description":"Structured learning paths for teams adopting AILK.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Course"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Courses</h1><p data-testid="courses-description" class="mt-block-tight text-lead">Structured learning paths for teams adopting AILK.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
73
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Courses","description":"Structured learning paths for teams adopting AILK.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Course"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Courses</h1><p data-testid="courses-description" class="mt-block-tight text-lead">Structured learning paths for teams adopting AILK.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">## Courses
|
|
74
74
|
|
|
75
75
|
Body content.</div></div></article></div></div></div></main>"
|
|
76
76
|
`);
|
|
@@ -99,7 +99,7 @@ describe("courses/[slug] — snapshot regression", () => {
|
|
|
99
99
|
});
|
|
100
100
|
const { container } = render(element);
|
|
101
101
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
102
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules.","mainEntity":{"@context":"https://schema.org","@type":"Course","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Courses","item":"http://localhost:3000/en/courses"},{"@type":"ListItem","position":3,"name":"AEO Fundamentals","item":"http://localhost:3000/en/courses/aeo-fundamentals"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AEO Fundamentals</h1><p data-testid="course-description" class="mt-block-tight text-lead">A practical course covering Layer 1 AEO rules.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
102
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules.","mainEntity":{"@context":"https://schema.org","@type":"Course","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Courses","item":"http://localhost:3000/en/courses"},{"@type":"ListItem","position":3,"name":"AEO Fundamentals","item":"http://localhost:3000/en/courses/aeo-fundamentals"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AEO Fundamentals</h1><p data-testid="course-description" class="mt-block-tight text-lead">A practical course covering Layer 1 AEO rules.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">## AEO Fundamentals
|
|
103
103
|
|
|
104
104
|
Body content.</div></div></article></div></div></div></main>"
|
|
105
105
|
`);
|
package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -86,7 +86,7 @@ describe("DownloadsPage — snapshot regression", () => {
|
|
|
86
86
|
});
|
|
87
87
|
const { container } = render(element);
|
|
88
88
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
89
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Downloads","description":"Downloadable assets and templates.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"DigitalDocument"}]}}</script><main id="main"><div><h1>Downloads</h1><p data-testid="downloads-description" class="mt-block-tight text-lead">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
89
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Downloads","description":"Downloadable assets and templates.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"DigitalDocument"}]}}</script><main id="main"><div><h1>Downloads</h1><p data-testid="downloads-description" class="mt-block-tight text-lead">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">
|
|
90
90
|
Body.</div></article></div></div></div></main>"
|
|
91
91
|
`);
|
|
92
92
|
});
|
|
@@ -100,7 +100,7 @@ describe("EventsPage — snapshot regression", () => {
|
|
|
100
100
|
});
|
|
101
101
|
const { container } = render(element);
|
|
102
102
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
103
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Events","description":"Upcoming and past events.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Event"}]}}</script><main id="main"><div><h1>Events</h1><p data-testid="events-description" class="mt-block-tight text-lead">Upcoming and past events.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
103
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Events","description":"Upcoming and past events.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Event"}]}}</script><main id="main"><div><h1>Events</h1><p data-testid="events-description" class="mt-block-tight text-lead">Upcoming and past events.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">
|
|
104
104
|
Body.</div></article></div></div></div></main>"
|
|
105
105
|
`);
|
|
106
106
|
});
|
|
@@ -134,7 +134,7 @@ describe("EventSlugPage — snapshot regression", () => {
|
|
|
134
134
|
// This replaces the pre-migration hardcoded "event-description" testid
|
|
135
135
|
// with the slug-derived "annual-summit-description". Intentional per spec.
|
|
136
136
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
137
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Annual Summit","description":"Our annual developer summit.","mainEntity":{"@context":"https://schema.org","@type":"Event","name":"Annual Summit","description":"Our annual developer summit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Events","item":"http://localhost:3000/en/events"},{"@type":"ListItem","position":3,"name":"Annual Summit","item":"http://localhost:3000/en/events/annual-summit"}]}]}</script><main id="main"><div><h1>Annual Summit</h1><p data-testid="annual-summit-description" class="mt-block-tight text-lead">Our annual developer summit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
137
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Annual Summit","description":"Our annual developer summit.","mainEntity":{"@context":"https://schema.org","@type":"Event","name":"Annual Summit","description":"Our annual developer summit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Events","item":"http://localhost:3000/en/events"},{"@type":"ListItem","position":3,"name":"Annual Summit","item":"http://localhost:3000/en/events/annual-summit"}]}]}</script><main id="main"><div><h1>Annual Summit</h1><p data-testid="annual-summit-description" class="mt-block-tight text-lead">Our annual developer summit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">
|
|
138
138
|
Body.</div></article></div></div></div></main>"
|
|
139
139
|
`);
|
|
140
140
|
});
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* a `thankYou` block, so without this widening the callback would never be
|
|
36
36
|
* supplied on a recommending flow at all.
|
|
37
37
|
*
|
|
38
|
+
* Account link (#8771 D5): when the walk completes on an `invite` magnet (the
|
|
39
|
+
* step response's `next`), the client asks `accountInviteLinkAction` for the
|
|
40
|
+
* sign-in link with the email the visitor gave already filled in, then shows
|
|
41
|
+
* "Track this in your account" on `FlowStepper`'s own finished panel — or,
|
|
42
|
+
* for a flow with a `thankYou` block, hands the link to `/thanks/<slug>`
|
|
43
|
+
* through `sessionStorage` (`ailk:account-invite`). The email never enters a
|
|
44
|
+
* URL the walk navigates to (NFR1), and each link sits in a Session-Replay
|
|
45
|
+
* `data-sentry-block` element, since the scrub cannot reach a DOM `href`.
|
|
46
|
+
*
|
|
38
47
|
* Locale validation before interpolation (AILK #4707, security review on
|
|
39
48
|
* samhenry PR #163): the `[locale]` route segment is a Next.js dynamic
|
|
40
49
|
* segment — the framework matches ANY string there, and next-intl's
|
|
@@ -49,6 +58,7 @@
|
|
|
49
58
|
* bundle, the same reasoning `middleware.ts` already documents for the
|
|
50
59
|
* Edge runtime) before it ever reaches the template literal.
|
|
51
60
|
*/
|
|
61
|
+
import { Button, Link } from "@working-theory/ui";
|
|
52
62
|
import {
|
|
53
63
|
FlowStepper,
|
|
54
64
|
type FlowStepperMessages,
|
|
@@ -60,6 +70,7 @@ import {
|
|
|
60
70
|
deriveFlags,
|
|
61
71
|
encodePermalink,
|
|
62
72
|
flattenState,
|
|
73
|
+
flowStepNextSchema,
|
|
63
74
|
hiddenAnswers,
|
|
64
75
|
PERMALINK_VERSION,
|
|
65
76
|
pruneHiddenState,
|
|
@@ -68,8 +79,9 @@ import {
|
|
|
68
79
|
import { useRouter } from "next/navigation";
|
|
69
80
|
import { useMessages, useTranslations } from "next-intl";
|
|
70
81
|
import type { ReactElement } from "react";
|
|
71
|
-
import { useRef } from "react";
|
|
82
|
+
import { useRef, useState } from "react";
|
|
72
83
|
|
|
84
|
+
import { accountInviteLinkAction } from "./account-invite.actions";
|
|
73
85
|
import { submitFlowStepAction } from "./flow.actions";
|
|
74
86
|
|
|
75
87
|
interface Props {
|
|
@@ -88,6 +100,28 @@ function resolveSafeLocale(candidate: string): string {
|
|
|
88
100
|
return ENABLED_LOCALES.includes(candidate) ? candidate : DEFAULT_LOCALE;
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
/** The `/thanks` hand-off key (#8771 D5) — read once by `AccountInviteLink`. */
|
|
104
|
+
const ACCOUNT_INVITE_STORAGE_KEY = "ailk:account-invite";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* #8771 D5 — the email the engine will land as the lead's: the first
|
|
108
|
+
* `mapsTo: "email"` field of the submitted `field` step that holds a
|
|
109
|
+
* non-empty string. Mirrors `extractMappedEmail` (a later step wins).
|
|
110
|
+
*/
|
|
111
|
+
function mappedEmailOf(
|
|
112
|
+
config: FlowConfig,
|
|
113
|
+
payload: FlowStepSubmitPayload,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
const step = config.steps.find((s) => s.key === payload.stepKey);
|
|
116
|
+
if (step?.type !== "field") return undefined;
|
|
117
|
+
for (const field of step.fields) {
|
|
118
|
+
if (field.mapsTo !== "email") continue;
|
|
119
|
+
const value = payload.values[field.name];
|
|
120
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* D7 (#4770) — every question `name` contributed by a `segmentation`-typed
|
|
93
127
|
* step. This is the allowlist the encoded permalink is filtered to: a
|
|
@@ -130,6 +164,13 @@ export function FlowStepperClient({
|
|
|
130
164
|
// recommending completion can hand the walk's session to the results page
|
|
131
165
|
// via `sessionStorage` (never the URL — the permalink stays answers-only).
|
|
132
166
|
const lastSessionIdRef = useRef<string | undefined>(undefined);
|
|
167
|
+
// #8771 D5 — the email the walk has landed, and the account link built from
|
|
168
|
+
// it at completion on an `invite` magnet. The ref carries the link to
|
|
169
|
+
// `handleCompleted`; the state draws it on the finished panel.
|
|
170
|
+
const emailRef = useRef<string | undefined>(undefined);
|
|
171
|
+
const inviteHrefRef = useRef<string | null>(null);
|
|
172
|
+
const [inviteHref, setInviteHref] = useState<string | null>(null);
|
|
173
|
+
const tContact = useTranslations("contact");
|
|
133
174
|
|
|
134
175
|
async function handleSubmitStep(
|
|
135
176
|
payload: FlowStepSubmitPayload,
|
|
@@ -141,12 +182,36 @@ export function FlowStepperClient({
|
|
|
141
182
|
[payload.stepKey]: payload.values,
|
|
142
183
|
};
|
|
143
184
|
lastSessionIdRef.current = result.response.sessionId;
|
|
185
|
+
emailRef.current = mappedEmailOf(config, payload) ?? emailRef.current;
|
|
186
|
+
if (result.response.completed) {
|
|
187
|
+
await resolveInviteLink(result.response);
|
|
188
|
+
}
|
|
144
189
|
} else if (result.kind === "step_conflict") {
|
|
145
190
|
answersRef.current = {};
|
|
191
|
+
emailRef.current = undefined;
|
|
146
192
|
}
|
|
147
193
|
return result;
|
|
148
194
|
}
|
|
149
195
|
|
|
196
|
+
// #8771 D5 — on an `invite` completion with a landed email, ask the server
|
|
197
|
+
// for the prefilled sign-in link. A rejected or null answer gives no link.
|
|
198
|
+
async function resolveInviteLink(response: unknown): Promise<void> {
|
|
199
|
+
inviteHrefRef.current = null;
|
|
200
|
+
setInviteHref(null);
|
|
201
|
+
const next = flowStepNextSchema.safeParse(
|
|
202
|
+
(response as { next?: unknown }).next,
|
|
203
|
+
);
|
|
204
|
+
const email = emailRef.current;
|
|
205
|
+
if (!next.success || email === undefined || isRecommending) return;
|
|
206
|
+
try {
|
|
207
|
+
const href = await accountInviteLinkAction(email);
|
|
208
|
+
inviteHrefRef.current = href;
|
|
209
|
+
setInviteHref(href);
|
|
210
|
+
} catch {
|
|
211
|
+
// No link — the walk still completes.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
150
215
|
// #4663 D11 — `aeoScore` passes through untouched: this repo's segment
|
|
151
216
|
// redirect is unchanged; C2's results page (on samhenry) is the consumer
|
|
152
217
|
// of the score.
|
|
@@ -209,6 +274,26 @@ export function FlowStepperClient({
|
|
|
209
274
|
|
|
210
275
|
if (!thankYou) return;
|
|
211
276
|
const thankYouSlug = resolveThankYouSlug(thankYou, result.segment);
|
|
277
|
+
// #8771 D5 — hand the link to /thanks through the per-tab sessionStorage,
|
|
278
|
+
// never the URL; with no link, clear an earlier walk's so it never shows
|
|
279
|
+
// on this page. A storage that throws gives no link.
|
|
280
|
+
if (typeof window !== "undefined") {
|
|
281
|
+
try {
|
|
282
|
+
if (inviteHrefRef.current !== null) {
|
|
283
|
+
window.sessionStorage.setItem(
|
|
284
|
+
ACCOUNT_INVITE_STORAGE_KEY,
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
thanks: thankYouSlug,
|
|
287
|
+
href: inviteHrefRef.current,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
} else {
|
|
291
|
+
window.sessionStorage.removeItem(ACCOUNT_INVITE_STORAGE_KEY);
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// No hand-off.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
212
297
|
router.push(`/${resolveSafeLocale(locale)}/thanks/${thankYouSlug}`);
|
|
213
298
|
}
|
|
214
299
|
|
|
@@ -252,6 +337,25 @@ export function FlowStepperClient({
|
|
|
252
337
|
selectAllThatApply: t("selectAllThatApply"),
|
|
253
338
|
};
|
|
254
339
|
|
|
340
|
+
// #8771 D5 — the link goes INSIDE FlowStepper's own finished card (a flow
|
|
341
|
+
// with a `thankYou` block navigates away instead, so it never shows there).
|
|
342
|
+
// Session Replay records the DOM, so the link (it carries the email) is
|
|
343
|
+
// blocked from it; `Link` does not forward an undeclared data-* attribute.
|
|
344
|
+
const completedAction =
|
|
345
|
+
inviteHref !== null && thankYou === undefined && !isRecommending ? (
|
|
346
|
+
<div data-sentry-block>
|
|
347
|
+
<Button asChild variant="outline" className="mt-4 min-h-11">
|
|
348
|
+
<Link
|
|
349
|
+
href={inviteHref}
|
|
350
|
+
variant="bare"
|
|
351
|
+
data-testid="flow-account-invite"
|
|
352
|
+
>
|
|
353
|
+
{tContact("trackInAccount")}
|
|
354
|
+
</Link>
|
|
355
|
+
</Button>
|
|
356
|
+
</div>
|
|
357
|
+
) : undefined;
|
|
358
|
+
|
|
255
359
|
return (
|
|
256
360
|
<FlowStepper
|
|
257
361
|
config={config}
|
|
@@ -261,6 +365,7 @@ export function FlowStepperClient({
|
|
|
261
365
|
{...(thankYou !== undefined || isRecommending
|
|
262
366
|
? { onCompleted: handleCompleted }
|
|
263
367
|
: {})}
|
|
368
|
+
{...(completedAction !== undefined ? { completedAction } : {})}
|
|
264
369
|
/>
|
|
265
370
|
);
|
|
266
371
|
}
|
package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* #8771 AC-4 — the walk shows "Track this in your account" with the email the
|
|
4
|
+
* visitor gave already filled in, and keeps that email out of every URL it
|
|
5
|
+
* navigates to. FlowStepper is a stub that submits a configurable step and
|
|
6
|
+
* calls the real `onCompleted` it is given; the Server Actions are doubles.
|
|
7
|
+
*/
|
|
8
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
9
|
+
import userEvent from "@testing-library/user-event";
|
|
10
|
+
import type { FlowConfig } from "@working-theory/validation";
|
|
11
|
+
import type { ReactElement } from "react";
|
|
12
|
+
|
|
13
|
+
const mockPush = jest.fn();
|
|
14
|
+
jest.mock("next/navigation", () => ({
|
|
15
|
+
useRouter: () => ({ push: mockPush }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock("next-intl", () => ({
|
|
19
|
+
useTranslations: () => (key: string) => key,
|
|
20
|
+
useMessages: () => ({}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line no-restricted-syntax -- the boundary under test is the client wiring, not the actions
|
|
24
|
+
jest.mock("../flow.actions", () => ({ submitFlowStepAction: jest.fn() }));
|
|
25
|
+
// eslint-disable-next-line no-restricted-syntax -- see above
|
|
26
|
+
jest.mock("../account-invite.actions", () => ({ accountInviteLinkAction: jest.fn() }));
|
|
27
|
+
|
|
28
|
+
type Step = { stepKey: string; values: Record<string, unknown> };
|
|
29
|
+
let steps: Step[] = [];
|
|
30
|
+
|
|
31
|
+
interface StubProps {
|
|
32
|
+
onSubmitStep: (payload: Step & { locale?: string }) => Promise<{
|
|
33
|
+
ok: boolean;
|
|
34
|
+
response?: { completed: boolean; segment?: string };
|
|
35
|
+
}>;
|
|
36
|
+
onCompleted?: (result: { segment?: string }) => void;
|
|
37
|
+
completedAction?: ReactElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
jest.mock("@working-theory/ui-signal-collectors", () => ({
|
|
41
|
+
FlowStepper: (props: StubProps) => {
|
|
42
|
+
async function handleSubmit(): Promise<void> {
|
|
43
|
+
for (const step of steps) {
|
|
44
|
+
const result = await props.onSubmitStep({ ...step, locale: "en" });
|
|
45
|
+
if (result.ok && result.response?.completed) {
|
|
46
|
+
props.onCompleted?.(
|
|
47
|
+
result.response.segment !== undefined ? { segment: result.response.segment } : {},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// The real FlowStepper renders `completedAction` inside its finished card.
|
|
53
|
+
return require("react").createElement(
|
|
54
|
+
"div",
|
|
55
|
+
{ "data-testid": "mock-completed-card" },
|
|
56
|
+
require("react").createElement(
|
|
57
|
+
"button",
|
|
58
|
+
{ "data-testid": "mock-submit", onClick: handleSubmit },
|
|
59
|
+
"Submit",
|
|
60
|
+
),
|
|
61
|
+
props.completedAction,
|
|
62
|
+
) as ReactElement;
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
import { accountInviteLinkAction } from "../account-invite.actions";
|
|
67
|
+
import { submitFlowStepAction } from "../flow.actions";
|
|
68
|
+
import { FlowStepperClient } from "../FlowStepperClient";
|
|
69
|
+
|
|
70
|
+
const mockSubmit = submitFlowStepAction as jest.MockedFunction<typeof submitFlowStepAction>;
|
|
71
|
+
const mockLink = accountInviteLinkAction as jest.MockedFunction<typeof accountInviteLinkAction>;
|
|
72
|
+
|
|
73
|
+
const HREF = "/sign-in?next=%2Faccount&email=jane%40example.com";
|
|
74
|
+
const EMAIL = "jane@example.com";
|
|
75
|
+
|
|
76
|
+
function config(opts: { tagged: boolean; thankYou: boolean; secondEmail?: boolean }): FlowConfig {
|
|
77
|
+
return {
|
|
78
|
+
slug: "get-a-quote",
|
|
79
|
+
defaultLocale: "en",
|
|
80
|
+
steps: [
|
|
81
|
+
{
|
|
82
|
+
key: "contact",
|
|
83
|
+
type: "field",
|
|
84
|
+
copy: { title: { en: "Let's get started" } },
|
|
85
|
+
fields: [
|
|
86
|
+
{
|
|
87
|
+
name: "email",
|
|
88
|
+
inputType: "email",
|
|
89
|
+
required: true,
|
|
90
|
+
...(opts.tagged ? { mapsTo: "email" } : {}),
|
|
91
|
+
copy: { label: { en: "Email" } },
|
|
92
|
+
},
|
|
93
|
+
...(opts.secondEmail
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
name: "backup",
|
|
97
|
+
inputType: "email",
|
|
98
|
+
...(opts.tagged ? { mapsTo: "email" } : {}),
|
|
99
|
+
copy: { label: { en: "Backup" } },
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
...(opts.thankYou
|
|
107
|
+
? { thankYou: { default: "flow-thanks", bySegment: { qualified: "flow-thanks-qualified" } } }
|
|
108
|
+
: {}),
|
|
109
|
+
} as FlowConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ok(response: Record<string, unknown>) {
|
|
113
|
+
return {
|
|
114
|
+
ok: true as const,
|
|
115
|
+
response: {
|
|
116
|
+
sessionId: "s",
|
|
117
|
+
landed: "contact",
|
|
118
|
+
nextStepKey: null,
|
|
119
|
+
completed: true,
|
|
120
|
+
...response,
|
|
121
|
+
},
|
|
122
|
+
} as never;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function walk(cfg: FlowConfig): Promise<void> {
|
|
126
|
+
render(<FlowStepperClient config={cfg} locale="en" slug="get-a-quote" />);
|
|
127
|
+
await userEvent.click(screen.getByTestId("mock-submit"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
mockPush.mockClear();
|
|
132
|
+
mockSubmit.mockReset();
|
|
133
|
+
mockLink.mockReset();
|
|
134
|
+
mockLink.mockResolvedValue(HREF);
|
|
135
|
+
steps = [{ stepKey: "contact", values: { email: EMAIL } }];
|
|
136
|
+
window.sessionStorage.clear();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("the finished panel (no thankYou block)", () => {
|
|
140
|
+
it("shows the prefilled link inside a Replay-blocked element", async () => {
|
|
141
|
+
mockSubmit.mockResolvedValue(ok({ next: { completion: "invite" } }));
|
|
142
|
+
|
|
143
|
+
await walk(config({ tagged: true, thankYou: false }));
|
|
144
|
+
|
|
145
|
+
const link = await screen.findByTestId("flow-account-invite");
|
|
146
|
+
expect(link).toHaveAttribute("href", HREF);
|
|
147
|
+
expect(link).toHaveTextContent("trackInAccount");
|
|
148
|
+
expect(link.closest("[data-sentry-block]")).not.toBeNull();
|
|
149
|
+
// Inside the finished card, not a sibling after the frame (#8771 design review).
|
|
150
|
+
expect(link.closest('[data-testid="mock-completed-card"]')).not.toBeNull();
|
|
151
|
+
expect(mockLink).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(mockLink).toHaveBeenCalledWith(EMAIL);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("only an email-tagged field counts", async () => {
|
|
156
|
+
mockSubmit.mockResolvedValue(ok({ next: { completion: "invite" } }));
|
|
157
|
+
|
|
158
|
+
await walk(config({ tagged: false, thankYou: false }));
|
|
159
|
+
|
|
160
|
+
await waitFor(() => expect(mockSubmit).toHaveBeenCalled());
|
|
161
|
+
expect(mockLink).not.toHaveBeenCalled();
|
|
162
|
+
expect(screen.queryByTestId("flow-account-invite")).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("uses the step's first tagged email field", async () => {
|
|
166
|
+
steps = [{ stepKey: "contact", values: { email: "first@example.com", backup: "second@example.com" } }];
|
|
167
|
+
mockSubmit.mockResolvedValue(ok({ next: { completion: "invite" } }));
|
|
168
|
+
|
|
169
|
+
await walk(config({ tagged: true, thankYou: false, secondEmail: true }));
|
|
170
|
+
|
|
171
|
+
await screen.findByTestId("flow-account-invite");
|
|
172
|
+
expect(mockLink).toHaveBeenCalledWith("first@example.com");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it.each([
|
|
176
|
+
["no next", { }],
|
|
177
|
+
["a step that does not complete", { completed: false, next: { completion: "invite" } }],
|
|
178
|
+
])("%s: no action call and no link", async (_name, response) => {
|
|
179
|
+
mockSubmit.mockResolvedValue(ok(response));
|
|
180
|
+
|
|
181
|
+
await walk(config({ tagged: true, thankYou: false }));
|
|
182
|
+
|
|
183
|
+
await waitFor(() => expect(mockSubmit).toHaveBeenCalled());
|
|
184
|
+
expect(mockLink).not.toHaveBeenCalled();
|
|
185
|
+
expect(screen.queryByTestId("flow-account-invite")).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("an action answer of null shows no link", async () => {
|
|
189
|
+
mockSubmit.mockResolvedValue(ok({ next: { completion: "invite" } }));
|
|
190
|
+
mockLink.mockResolvedValue(null);
|
|
191
|
+
|
|
192
|
+
await walk(config({ tagged: true, thankYou: false }));
|
|
193
|
+
|
|
194
|
+
await waitFor(() => expect(mockLink).toHaveBeenCalled());
|
|
195
|
+
expect(screen.queryByTestId("flow-account-invite")).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("a conflict forgets the email: a later completing step with no email field asks for no link", async () => {
|
|
199
|
+
mockSubmit
|
|
200
|
+
.mockResolvedValueOnce(ok({ completed: false }))
|
|
201
|
+
.mockResolvedValueOnce({ ok: false, kind: "step_conflict", message: "x" } as never)
|
|
202
|
+
.mockResolvedValueOnce(ok({ next: { completion: "invite" } }));
|
|
203
|
+
steps = [
|
|
204
|
+
{ stepKey: "contact", values: { email: EMAIL } },
|
|
205
|
+
{ stepKey: "contact", values: { email: EMAIL } },
|
|
206
|
+
{ stepKey: "other", values: { note: "hi" } },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
await walk(config({ tagged: true, thankYou: false }));
|
|
210
|
+
|
|
211
|
+
await waitFor(() => expect(mockSubmit).toHaveBeenCalledTimes(3));
|
|
212
|
+
expect(mockLink).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("the /thanks hand-off (a thankYou block)", () => {
|
|
217
|
+
it("writes one sessionStorage item and pushes a URL with no email in it", async () => {
|
|
218
|
+
mockSubmit.mockResolvedValue(ok({ segment: "qualified", next: { completion: "invite" } }));
|
|
219
|
+
|
|
220
|
+
await walk(config({ tagged: true, thankYou: true }));
|
|
221
|
+
|
|
222
|
+
await waitFor(() => expect(mockPush).toHaveBeenCalled());
|
|
223
|
+
expect(JSON.parse(window.sessionStorage.getItem("ailk:account-invite") ?? "null")).toEqual({
|
|
224
|
+
thanks: "flow-thanks-qualified",
|
|
225
|
+
href: HREF,
|
|
226
|
+
});
|
|
227
|
+
const url = mockPush.mock.calls[0]?.[0] as string;
|
|
228
|
+
expect(url).toBe("/en/thanks/flow-thanks-qualified");
|
|
229
|
+
expect(url).not.toContain("@");
|
|
230
|
+
expect(screen.queryByTestId("flow-account-invite")).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("clears an earlier walk's hand-off when this completion has no link", async () => {
|
|
234
|
+
window.sessionStorage.setItem(
|
|
235
|
+
"ailk:account-invite",
|
|
236
|
+
JSON.stringify({ thanks: "flow-thanks", href: HREF }),
|
|
237
|
+
);
|
|
238
|
+
mockSubmit.mockResolvedValue(ok({}));
|
|
239
|
+
|
|
240
|
+
await walk(config({ tagged: true, thankYou: true }));
|
|
241
|
+
|
|
242
|
+
await waitFor(() => expect(mockPush).toHaveBeenCalled());
|
|
243
|
+
expect(window.sessionStorage.getItem("ailk:account-invite")).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("a sessionStorage that throws still navigates", async () => {
|
|
247
|
+
mockSubmit.mockResolvedValue(ok({ next: { completion: "invite" } }));
|
|
248
|
+
const spy = jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
249
|
+
throw new Error("denied");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await walk(config({ tagged: true, thankYou: true }));
|
|
253
|
+
|
|
254
|
+
await waitFor(() => expect(mockPush).toHaveBeenCalledWith("/en/thanks/flow-thanks"));
|
|
255
|
+
spy.mockRestore();
|
|
256
|
+
});
|
|
257
|
+
});
|