create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- 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 +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authority list route tests: ClientsList + TestimonialsList.
|
|
3
|
+
* These are list-only routes (no [slug]/ detail) mirroring the products/services pattern.
|
|
4
|
+
* getMdxSource is injected via optional props on each page component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { render, screen } from "@testing-library/react";
|
|
8
|
+
|
|
9
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
MDXRemote: ({ source }: { source: string }): React.ReactElement => (
|
|
12
|
+
<div data-testid="mdx-rendered">{source}</div>
|
|
13
|
+
),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
|
|
18
|
+
import ClientsPage from "../app/[locale]/clients/page";
|
|
19
|
+
import TestimonialsPage from "../app/[locale]/testimonials/page";
|
|
20
|
+
|
|
21
|
+
import type { getMdxSource } from "@working-theory/page-renderer";
|
|
22
|
+
|
|
23
|
+
type GetMdxSource = typeof getMdxSource;
|
|
24
|
+
|
|
25
|
+
async function renderAsync(
|
|
26
|
+
element: Promise<React.ReactElement> | React.ReactElement,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const resolved = await element;
|
|
29
|
+
render(resolved);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("apps/web — Authority list routes (ClientsList + TestimonialsList)", () => {
|
|
33
|
+
// ── /clients ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe("/clients", () => {
|
|
36
|
+
describe("with ClientsList MDX present", () => {
|
|
37
|
+
const stubGetMdxSource = jest
|
|
38
|
+
.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>()
|
|
39
|
+
.mockResolvedValue({
|
|
40
|
+
frontmatter: {
|
|
41
|
+
type: "ClientsList",
|
|
42
|
+
slug: "clients",
|
|
43
|
+
title: "Clients",
|
|
44
|
+
description: "Organizations using AI Launch Kit.",
|
|
45
|
+
},
|
|
46
|
+
body: "# Clients\n\nBody.",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
beforeEach(() => stubGetMdxSource.mockClear());
|
|
50
|
+
|
|
51
|
+
it("renders the h1 from MDX frontmatter", async () => {
|
|
52
|
+
await renderAsync(
|
|
53
|
+
ClientsPage({
|
|
54
|
+
params: Promise.resolve({ locale: "en" }),
|
|
55
|
+
getMdxSource: stubGetMdxSource,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
59
|
+
"Clients",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("emits a JSON-LD script tag with @type CollectionPage", async () => {
|
|
64
|
+
await renderAsync(
|
|
65
|
+
ClientsPage({
|
|
66
|
+
params: Promise.resolve({ locale: "en" }),
|
|
67
|
+
getMdxSource: stubGetMdxSource,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
const script = screen.getByTestId("json-ld");
|
|
71
|
+
expect(script.getAttribute("type")).toBe("application/ld+json");
|
|
72
|
+
const parsed = JSON.parse(script.innerHTML) as { "@type": string };
|
|
73
|
+
expect(parsed["@type"]).toBe("CollectionPage");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("calls getMdxSource with ClientsList type, clients slug, and locale", async () => {
|
|
77
|
+
await renderAsync(
|
|
78
|
+
ClientsPage({
|
|
79
|
+
params: Promise.resolve({ locale: "en" }),
|
|
80
|
+
getMdxSource: stubGetMdxSource,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
expect(stubGetMdxSource).toHaveBeenCalledWith(
|
|
84
|
+
"ClientsList",
|
|
85
|
+
"clients",
|
|
86
|
+
"en",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders the description and MDX body", async () => {
|
|
91
|
+
await renderAsync(
|
|
92
|
+
ClientsPage({
|
|
93
|
+
params: Promise.resolve({ locale: "en" }),
|
|
94
|
+
getMdxSource: stubGetMdxSource,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(screen.getByTestId("clients-description")).toHaveTextContent(
|
|
98
|
+
"Organizations using AI Launch Kit.",
|
|
99
|
+
);
|
|
100
|
+
expect(screen.getByTestId("mdx-body")).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("with no ClientsList MDX (empty state)", () => {
|
|
105
|
+
const stubGetMdxSource = jest
|
|
106
|
+
.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>()
|
|
107
|
+
.mockResolvedValue(null);
|
|
108
|
+
|
|
109
|
+
it("renders the fallback sentinel without crashing", async () => {
|
|
110
|
+
await renderAsync(
|
|
111
|
+
ClientsPage({
|
|
112
|
+
params: Promise.resolve({ locale: "en" }),
|
|
113
|
+
getMdxSource: stubGetMdxSource,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── /testimonials ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("/testimonials", () => {
|
|
124
|
+
describe("with TestimonialsList MDX present", () => {
|
|
125
|
+
const stubGetMdxSource = jest
|
|
126
|
+
.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>()
|
|
127
|
+
.mockResolvedValue({
|
|
128
|
+
frontmatter: {
|
|
129
|
+
type: "TestimonialsList",
|
|
130
|
+
slug: "testimonials",
|
|
131
|
+
title: "Testimonials",
|
|
132
|
+
description: "What teams say about AI Launch Kit.",
|
|
133
|
+
},
|
|
134
|
+
body: "# Testimonials\n\nBody.",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
beforeEach(() => stubGetMdxSource.mockClear());
|
|
138
|
+
|
|
139
|
+
it("renders the h1 from MDX frontmatter", async () => {
|
|
140
|
+
await renderAsync(
|
|
141
|
+
TestimonialsPage({
|
|
142
|
+
params: Promise.resolve({ locale: "en" }),
|
|
143
|
+
getMdxSource: stubGetMdxSource,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
147
|
+
"Testimonials",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("emits a JSON-LD script tag with @type CollectionPage", async () => {
|
|
152
|
+
await renderAsync(
|
|
153
|
+
TestimonialsPage({
|
|
154
|
+
params: Promise.resolve({ locale: "en" }),
|
|
155
|
+
getMdxSource: stubGetMdxSource,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
const script = screen.getByTestId("json-ld");
|
|
159
|
+
expect(script.getAttribute("type")).toBe("application/ld+json");
|
|
160
|
+
const parsed = JSON.parse(script.innerHTML) as { "@type": string };
|
|
161
|
+
expect(parsed["@type"]).toBe("CollectionPage");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("calls getMdxSource with TestimonialsList type, testimonials slug, and locale", async () => {
|
|
165
|
+
await renderAsync(
|
|
166
|
+
TestimonialsPage({
|
|
167
|
+
params: Promise.resolve({ locale: "en" }),
|
|
168
|
+
getMdxSource: stubGetMdxSource,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
expect(stubGetMdxSource).toHaveBeenCalledWith(
|
|
172
|
+
"TestimonialsList",
|
|
173
|
+
"testimonials",
|
|
174
|
+
"en",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("renders the description and MDX body", async () => {
|
|
179
|
+
await renderAsync(
|
|
180
|
+
TestimonialsPage({
|
|
181
|
+
params: Promise.resolve({ locale: "en" }),
|
|
182
|
+
getMdxSource: stubGetMdxSource,
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
expect(
|
|
186
|
+
screen.getByTestId("testimonials-description"),
|
|
187
|
+
).toHaveTextContent("What teams say about AI Launch Kit.");
|
|
188
|
+
expect(screen.getByTestId("mdx-body")).toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("with no TestimonialsList MDX (empty state)", () => {
|
|
193
|
+
const stubGetMdxSource = jest
|
|
194
|
+
.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>()
|
|
195
|
+
.mockResolvedValue(null);
|
|
196
|
+
|
|
197
|
+
it("renders the fallback sentinel without crashing", async () => {
|
|
198
|
+
await renderAsync(
|
|
199
|
+
TestimonialsPage({
|
|
200
|
+
params: Promise.resolve({ locale: "en" }),
|
|
201
|
+
getMdxSource: stubGetMdxSource,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Tests for the env-driven Better Stack heartbeat util in apps/web (#1015).
|
|
2
|
+
// When BETTER_STACK_HEARTBEAT_URL is set, pingHeartbeat() POSTs to that URL;
|
|
3
|
+
// when unset it is a no-op. POST failures are handled gracefully — the function
|
|
4
|
+
// returns without throwing.
|
|
5
|
+
//
|
|
6
|
+
// Mocking global fetch is allowed (external module boundary).
|
|
7
|
+
// Mocking internal modules with `jest.mock('../foo')` is not allowed;
|
|
8
|
+
// this test mocks the fetch boundary instead.
|
|
9
|
+
|
|
10
|
+
const fetchMock = jest.fn();
|
|
11
|
+
global.fetch = fetchMock;
|
|
12
|
+
|
|
13
|
+
describe("apps/web — Better Stack heartbeat (#1015)", () => {
|
|
14
|
+
const originalHeartbeatUrl = process.env.BETTER_STACK_HEARTBEAT_URL;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
fetchMock.mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (originalHeartbeatUrl === undefined) {
|
|
22
|
+
delete process.env.BETTER_STACK_HEARTBEAT_URL;
|
|
23
|
+
} else {
|
|
24
|
+
process.env.BETTER_STACK_HEARTBEAT_URL = originalHeartbeatUrl;
|
|
25
|
+
}
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("is a no-op when BETTER_STACK_HEARTBEAT_URL is unset", async () => {
|
|
30
|
+
await jest.isolateModulesAsync(async () => {
|
|
31
|
+
delete process.env.BETTER_STACK_HEARTBEAT_URL;
|
|
32
|
+
const { pingHeartbeat } =
|
|
33
|
+
await import("../lib/observability/better-stack");
|
|
34
|
+
await pingHeartbeat();
|
|
35
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("POSTs to BETTER_STACK_HEARTBEAT_URL when set", async () => {
|
|
40
|
+
await jest.isolateModulesAsync(async () => {
|
|
41
|
+
process.env.BETTER_STACK_HEARTBEAT_URL =
|
|
42
|
+
"https://uptime.betterstack.com/api/v1/heartbeat/test-token";
|
|
43
|
+
fetchMock.mockResolvedValueOnce({ ok: true } as Response);
|
|
44
|
+
const { pingHeartbeat } =
|
|
45
|
+
await import("../lib/observability/better-stack");
|
|
46
|
+
await pingHeartbeat();
|
|
47
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
49
|
+
"https://uptime.betterstack.com/api/v1/heartbeat/test-token",
|
|
50
|
+
expect.objectContaining({ method: "POST" }),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("handles POST failure gracefully without throwing", async () => {
|
|
56
|
+
await jest.isolateModulesAsync(async () => {
|
|
57
|
+
process.env.BETTER_STACK_HEARTBEAT_URL =
|
|
58
|
+
"https://uptime.betterstack.com/api/v1/heartbeat/test-token";
|
|
59
|
+
fetchMock.mockRejectedValueOnce(new Error("network failure"));
|
|
60
|
+
const { pingHeartbeat } =
|
|
61
|
+
await import("../lib/observability/better-stack");
|
|
62
|
+
await expect(pingHeartbeat()).resolves.toBeUndefined();
|
|
63
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: source.config.ts blog dir resolves to apps/web/content/en/blog/.
|
|
3
|
+
*
|
|
4
|
+
* Regression guard for the bug introduced in PR #157 where `dir: "content/blog"`
|
|
5
|
+
* resolved relative to apps/web (cwd when fumadocs-mdx prebuild runs) instead of
|
|
6
|
+
* relative to the repo root. The fix uses an absolute path constructed from
|
|
7
|
+
* `import.meta.url` so the resolved dir is stable regardless of the cwd at
|
|
8
|
+
* build time.
|
|
9
|
+
*
|
|
10
|
+
* i18n Phase 3 S2 (2026-05-18) moved blog content from `content/blog/` to
|
|
11
|
+
* `content/en/blog/` as part of the locale-prefixed content layout.
|
|
12
|
+
*
|
|
13
|
+
* Issue #1459 (2026-05-25): per-app content directory divergence — blog content
|
|
14
|
+
* moved from `<repo-root>/content/en/blog/` to `apps/web/content/en/blog/`.
|
|
15
|
+
*
|
|
16
|
+
* Test approach: parse the `dir` value from source.config.ts statically and verify
|
|
17
|
+
* it resolves to a path that (a) is an absolute path and (b) points to the real
|
|
18
|
+
* apps/web/content/en/blog/ directory containing launching-ailk.mdx.
|
|
19
|
+
*
|
|
20
|
+
* Covers AC-373-1, AC-373-2, AC-373-4.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import * as path from "path";
|
|
25
|
+
|
|
26
|
+
const webRoot = path.resolve(__dirname, "..");
|
|
27
|
+
const blogDir = path.join(webRoot, "content", "en", "blog");
|
|
28
|
+
const sourceConfigPath = path.join(webRoot, "source.config.ts");
|
|
29
|
+
const sourceConfigSrc = fs.readFileSync(sourceConfigPath, "utf8");
|
|
30
|
+
|
|
31
|
+
describe("fix(web): fumadocs blog dir resolves to correct path (#373)", () => {
|
|
32
|
+
describe("AC-373-1: source.config.ts dir resolves to apps/web/content/en/blog/", () => {
|
|
33
|
+
it("source.config.ts imports fileURLToPath or uses an absolute-path strategy for dir", () => {
|
|
34
|
+
// The fix must not rely on a bare relative string like "content/en/blog"
|
|
35
|
+
// or "../../content/en/blog" — fumadocs-mdx resolves dir via
|
|
36
|
+
// path.resolve(dir) which anchors to process.cwd() (apps/web), not the
|
|
37
|
+
// config file location. A bare relative string that works only when
|
|
38
|
+
// cwd === repo-root is fragile. The correct pattern is to derive an
|
|
39
|
+
// absolute path from import.meta.url.
|
|
40
|
+
const hasImportMetaUrl = sourceConfigSrc.includes("import.meta.url");
|
|
41
|
+
const hasFileURLToPath =
|
|
42
|
+
sourceConfigSrc.includes("fileURLToPath") ||
|
|
43
|
+
sourceConfigSrc.includes("url.fileURLToPath");
|
|
44
|
+
expect(hasImportMetaUrl || hasFileURLToPath).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("source.config.ts dir value is an absolute path expression (not a bare relative string)", () => {
|
|
48
|
+
// The dir value must not be a literal bare string that caused the
|
|
49
|
+
// original regression — path.resolve() must be used.
|
|
50
|
+
expect(sourceConfigSrc).not.toContain('dir: "content/en/blog"');
|
|
51
|
+
expect(sourceConfigSrc).not.toContain("dir: 'content/en/blog'");
|
|
52
|
+
expect(sourceConfigSrc).not.toContain('dir: "content/blog"');
|
|
53
|
+
expect(sourceConfigSrc).not.toContain("dir: 'content/blog'");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("the resolved blog dir path exists on disk (apps/web/content/en/blog/)", () => {
|
|
57
|
+
expect(fs.existsSync(blogDir)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("AC-373-2: apps/web/content/en/blog/ contains the expected post", () => {
|
|
62
|
+
it("launching-ailk.mdx exists in apps/web/content/en/blog/", () => {
|
|
63
|
+
const filePath = path.join(blogDir, "launching-ailk.mdx");
|
|
64
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("launching-ailk.mdx has valid frontmatter (title, description, date)", () => {
|
|
68
|
+
const content = fs.readFileSync(
|
|
69
|
+
path.join(blogDir, "launching-ailk.mdx"),
|
|
70
|
+
"utf8",
|
|
71
|
+
);
|
|
72
|
+
expect(content).toContain("title:");
|
|
73
|
+
expect(content).toContain("description:");
|
|
74
|
+
expect(content).toContain("date:");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("AC-373-4: frontmatter schema in source.config.ts matches launching-ailk.mdx fields", () => {
|
|
79
|
+
it("source.config.ts schema includes title, description, date, updated, image", () => {
|
|
80
|
+
expect(sourceConfigSrc).toContain("title");
|
|
81
|
+
expect(sourceConfigSrc).toContain("description");
|
|
82
|
+
expect(sourceConfigSrc).toContain("date");
|
|
83
|
+
expect(sourceConfigSrc).toContain("updated");
|
|
84
|
+
expect(sourceConfigSrc).toContain("image");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: Fumadocs corpus populated after U1 content migration + U2 codegen wire-up.
|
|
3
|
+
*
|
|
4
|
+
* Requires `pnpm --filter @working-theory/web build` to have run first — the build
|
|
5
|
+
* step invokes `fumadocs-mdx/next` codegen and writes `apps/web/.source/`.
|
|
6
|
+
*
|
|
7
|
+
* fumadocs-core and fumadocs-mdx are ESM-only packages incompatible with
|
|
8
|
+
* ts-jest's CJS mode. We mock them and assert on the observable data-layer
|
|
9
|
+
* contract: the codegen output file exists, the source swap is in place,
|
|
10
|
+
* and the content/en/blog/ directory has the expected MDX entries (which the
|
|
11
|
+
* loader will serve at runtime). Per AC-U2-3, data-layer assertions are
|
|
12
|
+
* sufficient when a render harness cannot easily be wired up.
|
|
13
|
+
*
|
|
14
|
+
* i18n Phase 3 S2 (2026-05-18) moved blog content from `content/blog/` to
|
|
15
|
+
* `content/en/blog/`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
|
|
21
|
+
// Root of apps/web — two directories up from __tests__/
|
|
22
|
+
const webRoot = path.resolve(__dirname, "..");
|
|
23
|
+
const nextConfigSrc = fs.readFileSync(
|
|
24
|
+
path.join(webRoot, "next.config.mjs"),
|
|
25
|
+
"utf8",
|
|
26
|
+
);
|
|
27
|
+
const sourceSrc = fs.readFileSync(
|
|
28
|
+
path.join(webRoot, "lib", "source.ts"),
|
|
29
|
+
"utf8",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
describe("U2 codegen wire-up — data-layer assertions (AC-U2-3)", () => {
|
|
33
|
+
describe("AC-U2-1: next.config.mjs wraps with createMDX()", () => {
|
|
34
|
+
it("next.config.mjs imports createMDX from fumadocs-mdx/next", () => {
|
|
35
|
+
expect(nextConfigSrc).toContain('from "fumadocs-mdx/next"');
|
|
36
|
+
expect(nextConfigSrc).toContain("createMDX");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("next.config.mjs exports createMDX()(nextConfig)", () => {
|
|
40
|
+
expect(nextConfigSrc).toContain("createMDX()(nextConfig)");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("next.config.mjs preserves reactStrictMode: true", () => {
|
|
44
|
+
expect(nextConfigSrc).toContain("reactStrictMode: true");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("AC-U2-2: lib/source.ts swap completed", () => {
|
|
49
|
+
it("lib/source.ts does not contain the Phase 0 placeholder arrays", () => {
|
|
50
|
+
expect(sourceSrc).not.toContain("const docs: Parameters");
|
|
51
|
+
expect(sourceSrc).not.toContain("const meta: Parameters");
|
|
52
|
+
// Match the specific placeholder assignment pattern, not any occurrence of "= []"
|
|
53
|
+
expect(sourceSrc).not.toMatch(/\bconst (?:docs|meta)[^=]+=\s*\[\]/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("lib/source.ts imports blog from @/.source/server", () => {
|
|
57
|
+
// fumadocs-mdx@15 codegen splits output into server.ts / browser.ts /
|
|
58
|
+
// dynamic.ts (no top-level index.ts). The `blog` collection lives on
|
|
59
|
+
// the server entry point.
|
|
60
|
+
expect(sourceSrc).toContain('@/.source/server"');
|
|
61
|
+
expect(sourceSrc).toContain("blog");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("lib/source.ts no longer contains Phase 0 skeleton comments", () => {
|
|
65
|
+
expect(sourceSrc).not.toContain("Phase 0 skeleton");
|
|
66
|
+
expect(sourceSrc).not.toContain("Phase 2 swap target");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("AC-U2-4: codegen output exists and is gitignored", () => {
|
|
71
|
+
it(".source/server.ts exists after build (codegen ran)", () => {
|
|
72
|
+
// fumadocs-mdx@15 emits server.ts / browser.ts / dynamic.ts. server.ts
|
|
73
|
+
// is the canonical entry the loader consumes — its existence proves
|
|
74
|
+
// codegen ran.
|
|
75
|
+
const sourcePath = path.join(webRoot, ".source", "server.ts");
|
|
76
|
+
expect(fs.existsSync(sourcePath)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it(".source/ is listed in .gitignore", () => {
|
|
80
|
+
// Root .gitignore covers apps/web/.source/
|
|
81
|
+
const rootGitignore = fs.readFileSync(
|
|
82
|
+
path.resolve(webRoot, "..", "..", ".gitignore"),
|
|
83
|
+
"utf8",
|
|
84
|
+
);
|
|
85
|
+
expect(rootGitignore).toContain("apps/web/.source/");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("AC-U2-3: apps/web/content/en/blog/ corpus populated (issue #1459 path divergence)", () => {
|
|
90
|
+
// Issue #1459 (2026-05-25): blog content moved from <repo-root>/content/en/blog/
|
|
91
|
+
// to apps/web/content/en/blog/ (per-app content directory convention).
|
|
92
|
+
const blogDir = path.resolve(webRoot, "content", "en", "blog");
|
|
93
|
+
|
|
94
|
+
it("apps/web/content/en/blog/ directory exists", () => {
|
|
95
|
+
expect(fs.existsSync(blogDir)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("apps/web/content/en/blog/ contains at least one MDX file (corpus non-empty)", () => {
|
|
99
|
+
const files = fs.readdirSync(blogDir).filter((f) => f.endsWith(".mdx"));
|
|
100
|
+
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("launching-ailk.mdx exists in apps/web/content/en/blog/", () => {
|
|
104
|
+
const filePath = path.join(blogDir, "launching-ailk.mdx");
|
|
105
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("launching-ailk.mdx contains the expected title in frontmatter", () => {
|
|
109
|
+
const content = fs.readFileSync(
|
|
110
|
+
path.join(blogDir, "launching-ailk.mdx"),
|
|
111
|
+
"utf8",
|
|
112
|
+
);
|
|
113
|
+
expect(content).toContain("title: Launching AI Launch Kit");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("BLOG_EMPTY_SENTINEL export", () => {
|
|
118
|
+
it("page.tsx exports BLOG_EMPTY_SENTINEL as a non-empty string (smoke import check)", () => {
|
|
119
|
+
// Reading the file directly avoids loading RSC-incompatible next/navigation
|
|
120
|
+
// under jsdom. The sentinel value is a plain string constant.
|
|
121
|
+
const src = fs.readFileSync(
|
|
122
|
+
path.join(webRoot, "app", "[locale]", "blog", "[[...slug]]", "page.tsx"),
|
|
123
|
+
"utf8",
|
|
124
|
+
);
|
|
125
|
+
const match = src.match(/BLOG_EMPTY_SENTINEL\s*=\s*"([^"]+)"/);
|
|
126
|
+
expect(match).not.toBeNull();
|
|
127
|
+
expect(match![1]!.length).toBeGreaterThan(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* data-testid coverage tests for issue #518.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the five high-leverage user-flow elements carry the expected
|
|
5
|
+
* data-testid attributes. These selectors serve as stable anchors for the
|
|
6
|
+
* post-OSS e2e test suite (docs/internal/roadmaps/e2e-testing-roadmap.md §5.2).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { render, screen } from "@testing-library/react";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// ContactForm — submit button
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
// Mocks required by ContactForm (uses React 19 useFormState / useFormStatus)
|
|
17
|
+
jest.mock("react-dom", () => ({
|
|
18
|
+
...jest.requireActual("react-dom"),
|
|
19
|
+
useFormState: jest.fn((_action: unknown, init: unknown) => [init, jest.fn()]),
|
|
20
|
+
useFormStatus: jest.fn(() => ({ pending: false })),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock required to isolate ContactForm from server action; tracked in #507
|
|
24
|
+
jest.mock("../app/[locale]/contact/contact.actions", () => ({
|
|
25
|
+
captureLeadAction: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { ContactForm } from "../app/[locale]/contact/ContactForm";
|
|
29
|
+
|
|
30
|
+
describe("ContactForm — data-testid coverage", () => {
|
|
31
|
+
it("submit button has data-testid=contact-submit", () => {
|
|
32
|
+
render(<ContactForm />);
|
|
33
|
+
const btn = screen.getByTestId("contact-submit");
|
|
34
|
+
expect(btn).toBeInTheDocument();
|
|
35
|
+
expect(btn.tagName).toBe("BUTTON");
|
|
36
|
+
expect(btn).toHaveAttribute("type", "submit");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// SiteNav — nav link testids
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
import { SiteNav } from "../src/components/SiteNav";
|
|
45
|
+
|
|
46
|
+
describe("SiteNav — data-testid coverage", () => {
|
|
47
|
+
it("home link has data-testid=nav-home-link", () => {
|
|
48
|
+
render(<SiteNav />);
|
|
49
|
+
const link = screen.getByTestId("nav-home-link");
|
|
50
|
+
expect(link).toBeInTheDocument();
|
|
51
|
+
expect(link).toHaveAttribute("href", "/");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("contact link has data-testid=nav-contact-link", () => {
|
|
55
|
+
render(<SiteNav />);
|
|
56
|
+
const link = screen.getByTestId("nav-contact-link");
|
|
57
|
+
expect(link).toBeInTheDocument();
|
|
58
|
+
expect(link).toHaveAttribute("href", "/contact");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Tests for the env-driven Google Analytics component (#886).
|
|
2
|
+
// When NEXT_PUBLIC_GOOGLE_TAG_ID is set, the component renders two script
|
|
3
|
+
// elements: an external gtag.js loader and an inline init script. When unset,
|
|
4
|
+
// the component renders nothing. STEALTH state is intentionally not consulted
|
|
5
|
+
// — Sam's call per #886.
|
|
6
|
+
//
|
|
7
|
+
// next/script is mocked locally because Next's runtime script-injection
|
|
8
|
+
// machinery isn't available under jsdom; the mock renders a plain <script>
|
|
9
|
+
// element so RTL queries can observe the output. Mocking a third-party module
|
|
10
|
+
// (not an internal one) keeps the test within the no-restricted-syntax rule.
|
|
11
|
+
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import { render } from "@testing-library/react";
|
|
14
|
+
|
|
15
|
+
jest.mock("next/script", () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: ({
|
|
18
|
+
src,
|
|
19
|
+
id,
|
|
20
|
+
children,
|
|
21
|
+
}: {
|
|
22
|
+
src?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
strategy?: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}): React.ReactElement =>
|
|
27
|
+
React.createElement(
|
|
28
|
+
"script",
|
|
29
|
+
{ "data-testid": id ? `script-${id}` : "script-external", src, id },
|
|
30
|
+
children,
|
|
31
|
+
),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe("apps/web — GoogleAnalytics (#886)", () => {
|
|
35
|
+
const originalTagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
36
|
+
const originalStealth = process.env.STEALTH;
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (originalTagId === undefined) {
|
|
40
|
+
delete process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.NEXT_PUBLIC_GOOGLE_TAG_ID = originalTagId;
|
|
43
|
+
}
|
|
44
|
+
if (originalStealth === undefined) {
|
|
45
|
+
delete process.env.STEALTH;
|
|
46
|
+
} else {
|
|
47
|
+
process.env.STEALTH = originalStealth;
|
|
48
|
+
}
|
|
49
|
+
jest.resetModules();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders gtag.js loader + init script when NEXT_PUBLIC_GOOGLE_TAG_ID is set", () => {
|
|
53
|
+
jest.isolateModules(() => {
|
|
54
|
+
process.env.NEXT_PUBLIC_GOOGLE_TAG_ID = "G-MJFBVBHCK9";
|
|
55
|
+
|
|
56
|
+
const { GoogleAnalytics } = require("../components/google-analytics");
|
|
57
|
+
const { container } = render(React.createElement(GoogleAnalytics));
|
|
58
|
+
const scripts = container.querySelectorAll("script");
|
|
59
|
+
expect(scripts).toHaveLength(2);
|
|
60
|
+
const external = scripts[0]!;
|
|
61
|
+
expect(external.getAttribute("src")).toBe(
|
|
62
|
+
"https://www.googletagmanager.com/gtag/js?id=G-MJFBVBHCK9",
|
|
63
|
+
);
|
|
64
|
+
const init = scripts[1]!;
|
|
65
|
+
expect(init.getAttribute("id")).toBe("ga-init");
|
|
66
|
+
expect(init.textContent).toContain("gtag('config', 'G-MJFBVBHCK9')");
|
|
67
|
+
expect(init.textContent).toContain(
|
|
68
|
+
"window.dataLayer = window.dataLayer || []",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders nothing when NEXT_PUBLIC_GOOGLE_TAG_ID is unset", () => {
|
|
74
|
+
jest.isolateModules(() => {
|
|
75
|
+
delete process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
76
|
+
|
|
77
|
+
const { GoogleAnalytics } = require("../components/google-analytics");
|
|
78
|
+
const { container } = render(React.createElement(GoogleAnalytics));
|
|
79
|
+
expect(container.querySelectorAll("script")).toHaveLength(0);
|
|
80
|
+
expect(container.textContent).toBe("");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("renders nothing when NEXT_PUBLIC_GOOGLE_TAG_ID is empty string", () => {
|
|
85
|
+
jest.isolateModules(() => {
|
|
86
|
+
process.env.NEXT_PUBLIC_GOOGLE_TAG_ID = "";
|
|
87
|
+
|
|
88
|
+
const { GoogleAnalytics } = require("../components/google-analytics");
|
|
89
|
+
const { container } = render(React.createElement(GoogleAnalytics));
|
|
90
|
+
expect(container.querySelectorAll("script")).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders scripts regardless of STEALTH=true (analytics not gated on stealth)", () => {
|
|
95
|
+
jest.isolateModules(() => {
|
|
96
|
+
process.env.NEXT_PUBLIC_GOOGLE_TAG_ID = "G-TESTABC123";
|
|
97
|
+
process.env.STEALTH = "true";
|
|
98
|
+
|
|
99
|
+
const { GoogleAnalytics } = require("../components/google-analytics");
|
|
100
|
+
const { container } = render(React.createElement(GoogleAnalytics));
|
|
101
|
+
expect(container.querySelectorAll("script")).toHaveLength(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|