create-ailk 0.6.0 → 0.7.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 +44 -5
- package/component-catalog.json +146 -21
- package/component-catalog.md +9 -6
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +124 -13
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +76 -2
- package/dist/lib/extract-module-bundle.js +219 -8
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +27 -4
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +7 -0
- package/dist/parse-args.js +23 -1
- package/dist/programmatic.d.ts +46 -0
- package/dist/programmatic.js +144 -19
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +494 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +149 -28
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +58 -2
- package/dist/template-manifest.js +196 -69
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +162 -0
- package/site-templates/marketing-site/DESIGN.md +162 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +162 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/minimalism-site/DESIGN.md +162 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/site-templates/saas-product/DESIGN.md +162 -0
- package/site-templates/simple-service/DESIGN.md +162 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
- package/site-templates/simple-service/ailk-template.json +4 -3
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
- package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +8 -8
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +111 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +10 -5
- package/templates/DESIGN.md +162 -0
- package/templates/README.md +134 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
- package/templates/apps/api/src/config/index.ts +19 -6
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/server.ts +6 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
- package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +113 -51
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +64 -12
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +134 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +52 -37
- package/templates/apps/web/middleware.ts +1 -1
- 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 +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
- package/templates/content/_site.mdx +41 -3
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +105 -0
- package/templates/content/en/ContactPage/contact.mdx +44 -85
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +276 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
- package/templates/content/en/SoftwareProduct/product.mdx +175 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +3 -2
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ContactForm's consent row (#6466, spec AC-2 / AC-3).
|
|
3
|
+
*
|
|
4
|
+
* ContactForm is the only form a /contact visitor can submit — the fallback
|
|
5
|
+
* `ConversationalIntakeWrapper` swaps in on Skip or a failed auto-submit. Its
|
|
6
|
+
* consent line must be a real control: a visible, unticked checkbox named by
|
|
7
|
+
* a statement whose "Terms" is a link, and a submit button that stays
|
|
8
|
+
* disabled until the box is ticked (the form is `noValidate`, so `required`
|
|
9
|
+
* alone would gate nothing).
|
|
10
|
+
*
|
|
11
|
+
* jsdom lacks `useFormState`/`useFormStatus` (see
|
|
12
|
+
* page-renderer-migration.test.tsx), so `react-dom` is extended with inert
|
|
13
|
+
* stand-ins; the outermost network boundary (`@working-theory/api-client`)
|
|
14
|
+
* is mocked so the real server action module loads without a network.
|
|
15
|
+
*/
|
|
16
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
17
|
+
import { localeHref } from "@working-theory/ui";
|
|
18
|
+
|
|
19
|
+
const MESSAGES: Record<string, string> = {
|
|
20
|
+
consentStatement: "I accept the [Terms](terms).",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
jest.mock("next-intl", () => ({
|
|
24
|
+
useTranslations: () => (key: string) => MESSAGES[key] ?? key,
|
|
25
|
+
useLocale: () => "en",
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock("react-dom", () => ({
|
|
29
|
+
...jest.requireActual("react-dom"),
|
|
30
|
+
useFormState: (_action: unknown, initial: unknown) => [initial, jest.fn()],
|
|
31
|
+
useFormStatus: () => ({ pending: false }),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
35
|
+
leads: { submit: jest.fn() },
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import { ContactForm } from "../ContactForm.js";
|
|
39
|
+
|
|
40
|
+
// jsdom does not implement ResizeObserver — the Radix Checkbox renders a
|
|
41
|
+
// hidden form-bubble input that observes size. Polyfill with a no-op.
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
global.ResizeObserver = class ResizeObserver {
|
|
44
|
+
observe() {}
|
|
45
|
+
unobserve() {}
|
|
46
|
+
disconnect() {}
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("ContactForm consent row (#6466)", () => {
|
|
51
|
+
it("renders a visible, unticked checkbox (AC-2 i)", () => {
|
|
52
|
+
render(<ContactForm />);
|
|
53
|
+
const checkbox = screen.getByRole("checkbox");
|
|
54
|
+
expect(checkbox).toBeVisible();
|
|
55
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("links 'Terms' to the locale-aware /terms route (AC-2 ii)", () => {
|
|
59
|
+
render(<ContactForm />);
|
|
60
|
+
const link = screen.getByRole("link", { name: "Terms" });
|
|
61
|
+
expect(link).toHaveAttribute("href", localeHref("en", "/terms"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("names the checkbox by the consent statement (AC-2 iii)", () => {
|
|
65
|
+
render(<ContactForm />);
|
|
66
|
+
// jsdom's accname pads the inline link with a space ("Terms .").
|
|
67
|
+
expect(screen.getByRole("checkbox")).toHaveAccessibleName(
|
|
68
|
+
/^I accept the Terms ?\.$/,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("clicking the Terms link does not tick the box (AC-2 iv)", () => {
|
|
73
|
+
render(<ContactForm />);
|
|
74
|
+
const link = screen.getByRole("link", { name: "Terms" });
|
|
75
|
+
// A wrapping <label> is what would forward the click to the box.
|
|
76
|
+
expect(link.closest("label")).toBeNull();
|
|
77
|
+
link.addEventListener("click", (event) => event.preventDefault());
|
|
78
|
+
fireEvent.click(link);
|
|
79
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
80
|
+
"aria-checked",
|
|
81
|
+
"false",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps submit disabled until the box is ticked (AC-3)", () => {
|
|
86
|
+
render(<ContactForm />);
|
|
87
|
+
const submit = screen.getByTestId("contact-submit");
|
|
88
|
+
expect(submit).toBeDisabled();
|
|
89
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
90
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
91
|
+
"aria-checked",
|
|
92
|
+
"true",
|
|
93
|
+
);
|
|
94
|
+
expect(submit).toBeEnabled();
|
|
95
|
+
});
|
|
96
|
+
});
|
package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx
CHANGED
|
@@ -53,6 +53,7 @@ import { leads } from "@working-theory/api-client";
|
|
|
53
53
|
import { track } from "@working-theory/analytics/server";
|
|
54
54
|
|
|
55
55
|
import { ConversationalIntakeWrapper } from "../ConversationalIntakeWrapper.js";
|
|
56
|
+
import * as intakeAnalytics from "../intake-analytics";
|
|
56
57
|
|
|
57
58
|
const mockSubmit = leads.submit as jest.Mock;
|
|
58
59
|
const mockTrack = track as jest.Mock;
|
|
@@ -184,7 +185,7 @@ describe("ConversationalIntakeWrapper — Skip (AC2)", () => {
|
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)", () => {
|
|
187
|
-
it("fires one in_progress event per turn, then a terminal completed event, with increasing turnIndex", () => {
|
|
188
|
+
it("fires one in_progress event per turn, then a terminal completed event, with increasing turnIndex", async () => {
|
|
188
189
|
mockSubmit.mockResolvedValue({
|
|
189
190
|
ok: true,
|
|
190
191
|
body: { id: "lead_abc123", persisted: "db" },
|
|
@@ -198,6 +199,7 @@ describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)",
|
|
|
198
199
|
advanceThinking();
|
|
199
200
|
answer("jane@example.com");
|
|
200
201
|
advanceThinking();
|
|
202
|
+
await flushMicrotasks(); // recordIntakeTurn is dispatched on a later microtask (#6465)
|
|
201
203
|
|
|
202
204
|
const calls = mockTrack.mock.calls.map(([name, props]) => ({
|
|
203
205
|
name,
|
|
@@ -222,7 +224,7 @@ describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)",
|
|
|
222
224
|
}
|
|
223
225
|
});
|
|
224
226
|
|
|
225
|
-
it("tags an abandoned (skipped) session with the turn it stopped at", () => {
|
|
227
|
+
it("tags an abandoned (skipped) session with the turn it stopped at", async () => {
|
|
226
228
|
render(<ConversationalIntakeWrapper />);
|
|
227
229
|
advanceThinking();
|
|
228
230
|
answer("Jane Smith");
|
|
@@ -230,6 +232,7 @@ describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)",
|
|
|
230
232
|
|
|
231
233
|
fireEvent.click(screen.getByTestId(`${TEST_ID}-skip`));
|
|
232
234
|
advanceThinking();
|
|
235
|
+
await flushMicrotasks(); // recordIntakeTurn is dispatched on a later microtask (#6465)
|
|
233
236
|
|
|
234
237
|
const calls = mockTrack.mock.calls.map(([, props]) => props) as Array<{
|
|
235
238
|
status: string;
|
|
@@ -240,3 +243,82 @@ describe("ConversationalIntakeWrapper — per-turn abandonment analytics (AC3)",
|
|
|
240
243
|
expect(terminal?.turnIndex).toBeGreaterThan(0);
|
|
241
244
|
});
|
|
242
245
|
});
|
|
246
|
+
|
|
247
|
+
describe("ConversationalIntakeWrapper — a failing analytics call never strands the intake (#6465)", () => {
|
|
248
|
+
let recordSpy: jest.SpyInstance | undefined;
|
|
249
|
+
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
recordSpy?.mockRestore();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/** The Server Action reference throws synchronously at the call site. */
|
|
255
|
+
function spyRecordIntakeTurnThrowing(): void {
|
|
256
|
+
recordSpy = jest
|
|
257
|
+
.spyOn(intakeAnalytics, "recordIntakeTurn")
|
|
258
|
+
.mockImplementation(() => {
|
|
259
|
+
throw new Error("boom");
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
it("drives the intake from mount to its first question with Skip enabled", async () => {
|
|
264
|
+
spyRecordIntakeTurnThrowing();
|
|
265
|
+
|
|
266
|
+
render(<ConversationalIntakeWrapper />);
|
|
267
|
+
expect(screen.getByTestId(`${TEST_ID}-skip`)).toBeDisabled();
|
|
268
|
+
|
|
269
|
+
advanceThinking(); // turn 1 (name) revealed
|
|
270
|
+
await flushMicrotasks();
|
|
271
|
+
|
|
272
|
+
expect(screen.queryByTestId(`${TEST_ID}-thinking`)).toBeNull();
|
|
273
|
+
expect(screen.getByText("intake.namePrompt")).toBeInTheDocument();
|
|
274
|
+
expect(screen.getByTestId(`${TEST_ID}-skip`)).not.toBeDisabled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("dispatches the per-turn analytics call off the delay callback's synchronous path", async () => {
|
|
278
|
+
recordSpy = jest
|
|
279
|
+
.spyOn(intakeAnalytics, "recordIntakeTurn")
|
|
280
|
+
.mockResolvedValue(undefined);
|
|
281
|
+
|
|
282
|
+
render(<ConversationalIntakeWrapper />);
|
|
283
|
+
expect(recordSpy).toHaveBeenCalledTimes(0);
|
|
284
|
+
|
|
285
|
+
await flushMicrotasks();
|
|
286
|
+
expect(recordSpy).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(recordSpy.mock.calls[0]?.[0]).toMatchObject({
|
|
288
|
+
turnIndex: 1,
|
|
289
|
+
status: "in_progress",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("still falls back to the form on Skip", async () => {
|
|
294
|
+
spyRecordIntakeTurnThrowing();
|
|
295
|
+
|
|
296
|
+
render(<ConversationalIntakeWrapper />);
|
|
297
|
+
advanceThinking();
|
|
298
|
+
fireEvent.click(screen.getByTestId(`${TEST_ID}-skip`));
|
|
299
|
+
advanceThinking(); // skip message revealed, onSkip fires
|
|
300
|
+
await flushMicrotasks();
|
|
301
|
+
|
|
302
|
+
expect(screen.getByTestId("contact-form-stub")).toBeInTheDocument();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("still submits the lead on completion", async () => {
|
|
306
|
+
spyRecordIntakeTurnThrowing();
|
|
307
|
+
mockSubmit.mockResolvedValue({
|
|
308
|
+
ok: true,
|
|
309
|
+
body: { id: "lead_abc123", persisted: "db" },
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
render(<ConversationalIntakeWrapper />);
|
|
313
|
+
advanceThinking();
|
|
314
|
+
answer("Jane Smith");
|
|
315
|
+
advanceThinking();
|
|
316
|
+
answer("I'd like a demo.");
|
|
317
|
+
advanceThinking();
|
|
318
|
+
answer("jane@example.com");
|
|
319
|
+
advanceThinking(); // completion revealed, onComplete fires
|
|
320
|
+
await flushMicrotasks();
|
|
321
|
+
|
|
322
|
+
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -43,8 +43,7 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
43
43
|
}: {
|
|
44
44
|
children: React.ReactNode;
|
|
45
45
|
level: number;
|
|
46
|
-
}): React.ReactElement =>
|
|
47
|
-
React.createElement(`h${level}`, null, children),
|
|
46
|
+
}): React.ReactElement => React.createElement(`h${level}`, null, children),
|
|
48
47
|
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
49
48
|
React.createElement("div", {
|
|
50
49
|
"data-testid": "section-renderer",
|
|
@@ -59,8 +58,8 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
59
58
|
// is stable; the wrapper's own submission/skip/analytics wiring is covered
|
|
60
59
|
// by ConversationalIntakeWrapper.test.tsx.
|
|
61
60
|
// eslint-disable-next-line no-restricted-syntax -- test-seam mock required; composes ContactForm, which uses useFormState/useFormStatus that crash in jsdom; internal-module mock is the narrowest possible scope; tracked in #507
|
|
62
|
-
jest.mock("../
|
|
63
|
-
|
|
61
|
+
jest.mock("../ContactForm", () => ({
|
|
62
|
+
ContactForm: () =>
|
|
64
63
|
React.createElement("div", { "data-testid": "contact-form-stub" }),
|
|
65
64
|
}));
|
|
66
65
|
|
|
@@ -82,7 +81,7 @@ describe("ContactPage — snapshot regression", () => {
|
|
|
82
81
|
container.querySelector('[data-testid="contact-form-stub"]'),
|
|
83
82
|
).not.toBeNull();
|
|
84
83
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
85
|
-
`"<main><h1>Contact</h1><p data-testid="section-empty-state">Contact page content coming soon.</p></main><div data-testid="contact-form-stub"></div>"`,
|
|
84
|
+
`"<main id="main"><h1>Contact</h1><p data-testid="section-empty-state">Contact page content coming soon.</p></main><div data-testid="contact-form-stub"></div>"`,
|
|
86
85
|
);
|
|
87
86
|
});
|
|
88
87
|
|
|
@@ -110,7 +109,7 @@ describe("ContactPage — snapshot regression", () => {
|
|
|
110
109
|
container.querySelector('[data-testid="contact-form-stub"]'),
|
|
111
110
|
).not.toBeNull();
|
|
112
111
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
113
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"ContactPage","inLanguage":"en","name":"Contact Us","description":"Get in touch.","mainEntity":{"@type":"ContactPoint","contactType":"customer support"}}</script><main><h1>Contact Us</h1><p data-testid="contact-description">Get in touch.</p><div data-testid="section-renderer" data-section-count="0"></div></main><div data-testid="contact-form-stub"></div>"`,
|
|
112
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"ContactPage","inLanguage":"en","name":"Contact Us","description":"Get in touch.","mainEntity":{"@type":"ContactPoint","contactType":"customer support"}}</script><main id="main"><h1>Contact Us</h1><p data-testid="contact-description">Get in touch.</p><div data-testid="section-renderer" data-section-count="0"></div></main><div data-testid="contact-form-stub"></div>"`,
|
|
114
113
|
);
|
|
115
114
|
});
|
|
116
115
|
});
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
// Contact page. Renders sections via renderPage (sections mode)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Contact page. Renders sections via renderPage (sections mode) with the
|
|
2
|
+
// plain ContactForm slotted into the split hero's right column (or appended
|
|
3
|
+
// below when no split hero leads). The chat-style intake
|
|
4
|
+
// (ConversationalIntakeWrapper, #4534) remains available to a site that
|
|
5
|
+
// prefers it. JSON-LD and translations handled by renderPage.
|
|
5
6
|
import type {
|
|
6
7
|
getMdxSource as defaultGetMdxSource,
|
|
7
8
|
getPageSections as defaultGetPageSections,
|
|
8
9
|
} from "@working-theory/page-renderer";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getMdxSource as loadMdxSource,
|
|
12
|
+
getPageSections as loadPageSections,
|
|
13
|
+
renderPage,
|
|
14
|
+
renderPageMetadata,
|
|
15
|
+
} from "@working-theory/page-renderer";
|
|
10
16
|
import type { Metadata } from "next";
|
|
11
17
|
import type { ReactElement } from "react";
|
|
12
18
|
|
|
13
|
-
import {
|
|
19
|
+
import { ContactForm } from "./ContactForm";
|
|
14
20
|
|
|
15
21
|
interface Props {
|
|
16
22
|
params: Promise<{ locale: string }>;
|
|
@@ -31,6 +37,17 @@ export default async function Page({
|
|
|
31
37
|
getPageSections,
|
|
32
38
|
}: Props): Promise<ReactElement> {
|
|
33
39
|
const { locale } = await params;
|
|
40
|
+
// The contact form sits in the hero's right column when the page leads with
|
|
41
|
+
// a split hero (the two-column contact layout — operator ruling 2026-09-23:
|
|
42
|
+
// the Free template shows the plain form; the guided intake stays a Pro
|
|
43
|
+
// option); otherwise — no content, or a different lead section — it renders
|
|
44
|
+
// below the page, so the contact surface is never lost.
|
|
45
|
+
const source = await (getMdxSource ?? loadMdxSource)("ContactPage", "contact", locale);
|
|
46
|
+
const sections = source ? await (getPageSections ?? loadPageSections)(source.frontmatter) : [];
|
|
47
|
+
const lead = sections[0];
|
|
48
|
+
const slotted =
|
|
49
|
+
lead?.type === "hero" && lead.data.variant === "split" ? lead.id : undefined;
|
|
50
|
+
const intake = <ContactForm />;
|
|
34
51
|
const rendered = await renderPage({
|
|
35
52
|
type: "ContactPage",
|
|
36
53
|
slug: "contact",
|
|
@@ -38,11 +55,12 @@ export default async function Page({
|
|
|
38
55
|
translationsNamespace: "contact",
|
|
39
56
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
40
57
|
...(getPageSections !== undefined && { getPageSections }),
|
|
58
|
+
...(slotted !== undefined && { sectionSlots: { [slotted]: intake } }),
|
|
41
59
|
});
|
|
42
60
|
return (
|
|
43
61
|
<>
|
|
44
62
|
{rendered}
|
|
45
|
-
|
|
63
|
+
{slotted === undefined ? intake : null}
|
|
46
64
|
</>
|
|
47
65
|
);
|
|
48
66
|
}
|
|
@@ -49,7 +49,7 @@ describe("courses/page — snapshot regression", () => {
|
|
|
49
49
|
});
|
|
50
50
|
const { container } = render(element);
|
|
51
51
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
52
|
-
`"<main><h1 class="text-heading-1
|
|
52
|
+
`"<main id="main"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Courses</h1><p data-testid="section-empty-state">Courses listing coming soon.</p></main>"`,
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
|
|
@@ -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><h1>Courses</h1><p data-testid="courses-description">Structured learning paths for teams adopting AILK.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## Courses
|
|
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"><h1>Courses</h1><p data-testid="courses-description">Structured learning paths for teams adopting AILK.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## Courses
|
|
74
74
|
|
|
75
75
|
Body content.</div></article></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><h1>AEO Fundamentals</h1><p data-testid="course-description">A practical course covering Layer 1 AEO rules.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## AEO Fundamentals
|
|
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"><h1>AEO Fundamentals</h1><p data-testid="course-description">A practical course covering Layer 1 AEO rules.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## AEO Fundamentals
|
|
103
103
|
|
|
104
104
|
Body content.</div></article></div></div></main>"
|
|
105
105
|
`);
|
package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -30,8 +30,7 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
30
30
|
}: {
|
|
31
31
|
children: React.ReactNode;
|
|
32
32
|
level: number;
|
|
33
|
-
}): React.ReactElement =>
|
|
34
|
-
React.createElement(`h${level}`, null, children),
|
|
33
|
+
}): React.ReactElement => React.createElement(`h${level}`, null, children),
|
|
35
34
|
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
36
35
|
React.createElement("div", {
|
|
37
36
|
"data-testid": "section-renderer",
|
|
@@ -53,7 +52,7 @@ describe("DownloadsPage — snapshot regression", () => {
|
|
|
53
52
|
});
|
|
54
53
|
const { container } = render(element);
|
|
55
54
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
56
|
-
`"<main><h1>Downloads</h1><p data-testid="section-empty-state">Downloads listing coming soon.</p></main>"`,
|
|
55
|
+
`"<main id="main"><h1>Downloads</h1><p data-testid="section-empty-state">Downloads listing coming soon.</p></main>"`,
|
|
57
56
|
);
|
|
58
57
|
});
|
|
59
58
|
|
|
@@ -74,7 +73,7 @@ describe("DownloadsPage — snapshot regression", () => {
|
|
|
74
73
|
});
|
|
75
74
|
const { container } = render(element);
|
|
76
75
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
77
|
-
"<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><h1>Downloads</h1><p data-testid="downloads-description">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Downloads
|
|
76
|
+
"<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"><h1>Downloads</h1><p data-testid="downloads-description">Downloadable assets and templates.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Downloads
|
|
78
77
|
|
|
79
78
|
Body.</div></article></div></div></main>"
|
|
80
79
|
`);
|
|
@@ -39,8 +39,7 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
39
39
|
}: {
|
|
40
40
|
children: React.ReactNode;
|
|
41
41
|
level: number;
|
|
42
|
-
}): React.ReactElement =>
|
|
43
|
-
React.createElement(`h${level}`, null, children),
|
|
42
|
+
}): React.ReactElement => React.createElement(`h${level}`, null, children),
|
|
44
43
|
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
45
44
|
React.createElement("div", {
|
|
46
45
|
"data-testid": "section-renderer",
|
|
@@ -66,7 +65,7 @@ describe("EventsPage — snapshot regression", () => {
|
|
|
66
65
|
});
|
|
67
66
|
const { container } = render(element);
|
|
68
67
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
69
|
-
`"<main><h1>Events</h1><p data-testid="section-empty-state">Events listing coming soon.</p></main>"`,
|
|
68
|
+
`"<main id="main"><h1>Events</h1><p data-testid="section-empty-state">Events listing coming soon.</p></main>"`,
|
|
70
69
|
);
|
|
71
70
|
});
|
|
72
71
|
|
|
@@ -87,7 +86,7 @@ describe("EventsPage — snapshot regression", () => {
|
|
|
87
86
|
});
|
|
88
87
|
const { container } = render(element);
|
|
89
88
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
90
|
-
"<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><h1>Events</h1><p data-testid="events-description">Upcoming and past events.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Events
|
|
89
|
+
"<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"><h1>Events</h1><p data-testid="events-description">Upcoming and past events.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Events
|
|
91
90
|
|
|
92
91
|
Body.</div></article></div></div></main>"
|
|
93
92
|
`);
|
|
@@ -122,7 +121,7 @@ describe("EventSlugPage — snapshot regression", () => {
|
|
|
122
121
|
// This replaces the pre-migration hardcoded "event-description" testid
|
|
123
122
|
// with the slug-derived "annual-summit-description". Intentional per spec.
|
|
124
123
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
125
|
-
"<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><h1>Annual Summit</h1><p data-testid="annual-summit-description">Our annual developer summit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Annual Summit
|
|
124
|
+
"<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"><h1>Annual Summit</h1><p data-testid="annual-summit-description">Our annual developer summit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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"># Annual Summit
|
|
126
125
|
|
|
127
126
|
Body.</div></article></div></div></main>"
|
|
128
127
|
`);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Glossary page (#6658 D5) — a GlossaryList at /glossary. Reads the
|
|
2
|
+
// `GlossaryList` MDX with slug `glossary` and emits a JSON-LD CollectionPage
|
|
3
|
+
// whose mainEntity is a DefinedTermSet built from its `glossary-index`
|
|
4
|
+
// sections. A module-less surface a template enables by naming it in its
|
|
5
|
+
// roster; with no content the route 404s. Mirrors use-cases/page.tsx: a
|
|
6
|
+
// sections-composed page renders its sections, a prose-only page its MDX body.
|
|
7
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
8
|
+
import {
|
|
9
|
+
getMdxSource as loadMdxSource,
|
|
10
|
+
renderPage,
|
|
11
|
+
renderPageMetadata,
|
|
12
|
+
} from "@working-theory/page-renderer";
|
|
13
|
+
import type { Metadata } from "next";
|
|
14
|
+
import type { ReactElement } from "react";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
params: Promise<{ locale: string }>;
|
|
18
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function generateMetadata({
|
|
22
|
+
params,
|
|
23
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
24
|
+
const { locale } = await params;
|
|
25
|
+
return renderPageMetadata({
|
|
26
|
+
type: "GlossaryList",
|
|
27
|
+
slug: "glossary",
|
|
28
|
+
locale,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default async function Page({
|
|
33
|
+
params,
|
|
34
|
+
getMdxSource,
|
|
35
|
+
}: Props): Promise<ReactElement> {
|
|
36
|
+
const { locale } = await params;
|
|
37
|
+
const source = await (getMdxSource ?? loadMdxSource)("GlossaryList", "glossary", locale);
|
|
38
|
+
const composed =
|
|
39
|
+
source !== null &&
|
|
40
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
41
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
42
|
+
0;
|
|
43
|
+
return renderPage({
|
|
44
|
+
type: "GlossaryList",
|
|
45
|
+
slug: "glossary",
|
|
46
|
+
locale,
|
|
47
|
+
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
48
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -58,7 +58,7 @@ describe("how-to/[slug] — snapshot regression", () => {
|
|
|
58
58
|
});
|
|
59
59
|
const { container } = render(element);
|
|
60
60
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
61
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@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":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</script><main><h1>Deploy AILK to Vercel</h1><p data-testid="how-to-description">Step-by-step guide to deploying AI Launch Kit on Vercel.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## Steps
|
|
61
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@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":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</script><main id="main"><h1>Deploy AILK to Vercel</h1><p data-testid="how-to-description">Step-by-step guide to deploying AI Launch Kit on Vercel.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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 transition-colors 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) hover:bg-(--color-surface-secondary) active:bg-gray-200 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">## Steps
|
|
62
62
|
|
|
63
63
|
Body content.</div></article></div></div></main>"
|
|
64
64
|
`);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// a directory listing. No dedicated list MDX type exists for HowTo; the list
|
|
3
3
|
// page is generated from the detail type's slug enumeration.
|
|
4
4
|
import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
|
|
5
|
+
import { LinkListItem, RowGroup } from "@working-theory/ui/blocks";
|
|
6
|
+
import { Container, Heading, Text } from "@working-theory/ui/primitives";
|
|
5
7
|
import { getTranslations } from "next-intl/server";
|
|
6
8
|
import type { ReactElement } from "react";
|
|
7
9
|
|
|
@@ -20,21 +22,29 @@ export default async function Page({
|
|
|
20
22
|
|
|
21
23
|
return (
|
|
22
24
|
<main>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
<Container className="py-section">
|
|
26
|
+
<Heading level={1}>{t("heading")}</Heading>
|
|
27
|
+
<div data-testid="how-to-description">
|
|
28
|
+
<Text>{t("description")}</Text>
|
|
29
|
+
</div>
|
|
30
|
+
{slugs.length === 0 ? (
|
|
31
|
+
<div data-testid="section-empty-state">
|
|
32
|
+
<Text>{t("emptyState")}</Text>
|
|
33
|
+
</div>
|
|
34
|
+
) : (
|
|
35
|
+
<RowGroup data-testid="how-to-list">
|
|
36
|
+
{slugs.map((slug) => (
|
|
37
|
+
<LinkListItem
|
|
38
|
+
key={slug}
|
|
39
|
+
href={`/${locale}/how-to/${slug}`}
|
|
40
|
+
label={slug}
|
|
41
|
+
className="px-4"
|
|
42
|
+
data-testid={`how-to-list-item-${slug}`}
|
|
43
|
+
/>
|
|
44
|
+
))}
|
|
45
|
+
</RowGroup>
|
|
46
|
+
)}
|
|
47
|
+
</Container>
|
|
38
48
|
</main>
|
|
39
49
|
);
|
|
40
50
|
}
|