create-ailk 0.13.0 → 0.15.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 +12 -4
- package/component-catalog.json +250 -36
- package/component-catalog.md +10 -6
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +34 -2
- package/dist/add-route.js +18 -2
- package/dist/apply-design.d.ts +32 -2
- package/dist/apply-design.js +155 -22
- package/dist/cli.js +24 -3
- package/dist/derive-architecture.d.ts +15 -2
- package/dist/derive-architecture.js +34 -6
- package/dist/design-compile.d.ts +70 -2
- package/dist/design-compile.js +97 -1
- package/dist/design-home.d.ts +74 -0
- package/dist/design-home.js +256 -0
- package/dist/design-package.d.ts +39 -2
- package/dist/design-package.js +224 -9
- package/dist/design-presets.d.ts +17 -1
- package/dist/design-presets.js +17 -5
- package/dist/design-scaffold.d.ts +6 -25
- package/dist/design-scaffold.js +26 -74
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/design-checks.d.ts +12 -1
- package/dist/lib/design-checks.js +38 -2
- package/dist/lib/extract-module-bundle.d.ts +11 -0
- package/dist/lib/extract-module-bundle.js +36 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/lib/module-license-gate.d.ts +9 -0
- package/dist/lib/module-license-gate.js +18 -0
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +6 -0
- package/dist/parse-args.d.ts +22 -0
- package/dist/parse-args.js +73 -0
- package/dist/programmatic.d.ts +20 -0
- package/dist/programmatic.js +180 -35
- package/dist/site-chrome.d.ts +42 -0
- package/dist/site-chrome.js +140 -2
- package/dist/site-fonts.d.ts +21 -0
- package/dist/site-fonts.js +54 -0
- package/dist/site-manifest.d.ts +12 -5
- package/dist/site-manifest.js +25 -11
- package/dist/site-roster.d.ts +16 -0
- package/dist/site-roster.js +81 -0
- package/dist/starter-grid.js +15 -0
- package/dist/surfaces.js +12 -0
- package/dist/sync-routes.js +11 -2
- package/dist/team-profiles.d.ts +29 -0
- package/dist/team-profiles.js +43 -0
- package/dist/topology.d.ts +131 -0
- package/dist/topology.js +533 -0
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
- package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
- package/site-templates/designs/minimalism/DESIGN.md +166 -36
- package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
- package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
- package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
- package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/templates/README.md +2 -0
- package/templates/apps/api/CLAUDE.md +6 -1
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +18 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +3 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/lead-routing.ts +148 -29
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/CLAUDE.md +12 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +163 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +68 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/layout.tsx +212 -57
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/page.tsx +37 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
- package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/not-found.tsx +47 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +89 -1
- package/templates/apps/web/app/openapi.json/route.ts +69 -1
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
- package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/profile-back-link.ts +51 -0
- package/templates/apps/web/lib/relay.ts +206 -2
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +15 -0
- package/templates/apps/web/lib/site-fonts.ts +15 -0
- package/templates/apps/web/lib/site-logo.tsx +39 -1
- package/templates/apps/web/lib/site-theme-head.ts +33 -0
- package/templates/apps/web/lib/site-theme.ts +13 -1
- package/templates/apps/web/lib/topology.generated.ts +23 -0
- package/templates/apps/web/messages/en.json +7 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +32 -0
- package/templates/database/__tests__/agents.db.test.ts +65 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/auth/schema.prisma +23 -0
- package/templates/database/inbox/schema.prisma +13 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -34
- package/site-templates/designs/minimalism/sample/README.md +0 -48
- package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// topology.generated.ts — how this MCP server resolves its workspace store.
|
|
2
|
+
//
|
|
3
|
+
// GENERATED by create-ailk for this site's topology (#7426). Do not edit:
|
|
4
|
+
// every scaffold rewrites it. A hub resolves its workspace through the
|
|
5
|
+
// database; a spoke (`--topology spoke`) has none, so its content tools
|
|
6
|
+
// read the spoke's own MDX and its writes go to the hub at AILK_API_URL.
|
|
7
|
+
import {
|
|
8
|
+
initMcpPublicWorkspace,
|
|
9
|
+
mcpPublicWorkspace,
|
|
10
|
+
type McpWorkspaceResolution,
|
|
11
|
+
} from "./public-workspace.js";
|
|
12
|
+
|
|
13
|
+
export { registerLicenseKeyStore } from "./license-key-store.js";
|
|
14
|
+
export type { McpWorkspaceResolution };
|
|
15
|
+
|
|
16
|
+
export const TOPOLOGY: "hub" | "spoke" = "hub";
|
|
17
|
+
|
|
18
|
+
export async function initPublicWorkspace(): Promise<void> {
|
|
19
|
+
await initMcpPublicWorkspace();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function publicWorkspace(): McpWorkspaceResolution {
|
|
23
|
+
return mcpPublicWorkspace();
|
|
24
|
+
}
|
|
@@ -132,6 +132,16 @@ same `captureLeadAction`, and on Skip or a failed submit swaps to
|
|
|
132
132
|
embedding for it is behind the hero CTA's lightbox gate
|
|
133
133
|
(`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
|
|
134
134
|
|
|
135
|
+
## The same-origin relay (`/v1/*`)
|
|
136
|
+
|
|
137
|
+
A visitor's browser never calls the hub. Lead capture, newsletter signup, callback, scheduling and the commerce invoice call post to this site's own `/v1/...` address, and `app/v1/[...path]/route.ts` hands each request to `lib/relay.ts` (`handleRelay`), which forwards it server-side through `@working-theory/api-client`'s `relayToApi` with the site's own `AILK_SPOKE_KEY` when it has one (#7644). The browser never holds the key, so a hub can refuse unknown callers (#7633) without taking a site's forms down.
|
|
138
|
+
|
|
139
|
+
- **Only the list.** `RELAY_ROUTES` is the fixed set of (method, path) pairs: `POST /v1/leads/{submit,subscribe,callback}`, `GET /v1/schedule/availability`, `POST /v1/schedule/book`, `POST /v1/commerce/invoices`. Anything else is a 404 with no hub call. Adding a route is a reviewed change to that list, its test and `.env.example`.
|
|
140
|
+
- **No cross-site POST.** A `Sec-Fetch-Site` other than `same-origin`, or an `Origin` not on this site's own host, is refused 403 before the hub is called.
|
|
141
|
+
- **Headers are an allow-list.** The visitor's `x-forwarded-for` (else `x-real-ip`) and `user-agent` pass through so per-visitor rate limits still count each visitor; no `authorization`, client-sent `x-ailk-*` or `host` is forwarded, and a `cookie` goes only on `POST /v1/commerce/invoices`, the one session route.
|
|
142
|
+
- **The relay has its own per-visitor budget (#7707).** Each visitor gets 8 calls per 10 minutes across the five visitor routes, and the session route (`POST /v1/commerce/invoices`) gets its own, wider budget of 30 per 10 minutes; the call that exceeds either gets `429 rate_limited` before any hub call. The budget is held in this instance's own process (no shared store, no env variable) and holds only where the site's edge overwrites `x-forwarded-for` and `x-real-ip` — Vercel does; a bare self-hosted Next server does not.
|
|
143
|
+
- `/v1` is exempt from next-intl's locale rewrite in `middleware.ts`, as `/api` is.
|
|
144
|
+
|
|
135
145
|
## AEO & performance
|
|
136
146
|
|
|
137
147
|
- Static rendering is the default. Don't introduce client rendering without a reason.
|
|
@@ -147,3 +157,5 @@ embedding for it is behind the hero CTA's lightbox gate
|
|
|
147
157
|
- Don't use `transition-all`.
|
|
148
158
|
- Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
|
|
149
159
|
- Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
|
|
160
|
+
|
|
161
|
+
Last updated: 2026-10-05 — doc-sync: added § The same-origin relay (#7644) — verified against `apps/web/lib/relay.ts` and `apps/web/app/v1/[...path]/route.ts`.
|
|
@@ -89,6 +89,16 @@ jest.mock("next/navigation", () => ({
|
|
|
89
89
|
redirect: jest.fn(),
|
|
90
90
|
}));
|
|
91
91
|
|
|
92
|
+
// The colour-mode cases test the theme's modes, so they run against an unpinned
|
|
93
|
+
// chrome: a scaffold whose design pins light mode (minimalism, #7742 D2) would
|
|
94
|
+
// otherwise hide the control in every case. theme-modes.test.ts covers the pin.
|
|
95
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: `site-chrome.ts` is a generated data module with no external boundary to mock instead (as theme-modes.test.ts).
|
|
96
|
+
jest.mock("../../../../../lib/site-chrome", () => {
|
|
97
|
+
const actual = jest.requireActual("../../../../../lib/site-chrome");
|
|
98
|
+
const { colorMode, ...unpinned } = actual.siteChrome;
|
|
99
|
+
return { ...actual, siteChrome: unpinned };
|
|
100
|
+
});
|
|
101
|
+
|
|
92
102
|
import Page from "../settings/page";
|
|
93
103
|
|
|
94
104
|
const SESSION = {
|
package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `siteThemeModeCount` (#7742 D2 / AC-2): a design pinned to light has one
|
|
3
|
+
* colour mode whatever its palette declares; without the pin, the declared
|
|
4
|
+
* palette decides, as before.
|
|
5
|
+
*/
|
|
6
|
+
import type { SiteChrome } from "../../../../../../lib/site-chrome";
|
|
7
|
+
|
|
8
|
+
const mockSiteChrome: SiteChrome = {
|
|
9
|
+
nav: "full",
|
|
10
|
+
footer: "columns",
|
|
11
|
+
announcementBanner: false,
|
|
12
|
+
home: "default",
|
|
13
|
+
};
|
|
14
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: `site-chrome.ts` is a generated data module with no external boundary to mock instead; this is the seam theme-modes.ts itself reads.
|
|
15
|
+
jest.mock("../../../../../../lib/site-chrome", () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
siteChrome: mockSiteChrome,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Both palettes declared — the count is 2 without the pin.
|
|
21
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: the one seam onto `_site.mdx`; the palette is the test's to set.
|
|
22
|
+
jest.mock("../../../../../../lib/site-theme", () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
loadSiteBrandTokens: () => ({ colors: { light: { bg: "#ffffff" }, dark: { bg: "#000000" } } }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { siteThemeModeCount } from "../theme-modes";
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
delete mockSiteChrome.colorMode;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("siteThemeModeCount (#7742 D2)", () => {
|
|
34
|
+
it('colorMode: "light" gives 1 even when both palettes are declared', () => {
|
|
35
|
+
mockSiteChrome.colorMode = "light";
|
|
36
|
+
expect(siteThemeModeCount()).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("without colorMode the same palette gives 2", () => {
|
|
40
|
+
expect(siteThemeModeCount()).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
*
|
|
15
15
|
* A theme the loader rejects counts as the default (2): the layout surfaces
|
|
16
16
|
* that error itself, and settings must not be where it first appears.
|
|
17
|
+
*
|
|
18
|
+
* A design that pins the mode (`colorMode: "light"`, #7742 D2) has one mode
|
|
19
|
+
* whatever its palette declares, so the settings control hides there too.
|
|
17
20
|
*/
|
|
18
21
|
|
|
22
|
+
import { siteChrome, type SiteChrome } from "../../../../../lib/site-chrome";
|
|
19
23
|
import { loadSiteBrandTokens } from "../../../../../lib/site-theme";
|
|
20
24
|
|
|
25
|
+
/** Widened as the locale layout reads it: an older generated file has no `colorMode`. */
|
|
26
|
+
const chrome: SiteChrome & { colorMode?: "light" } = siteChrome;
|
|
27
|
+
|
|
21
28
|
export function siteThemeModeCount(): 1 | 2 {
|
|
29
|
+
if (chrome.colorMode === "light") return 1;
|
|
22
30
|
try {
|
|
23
31
|
const colors = loadSiteBrandTokens()?.["colors"];
|
|
24
32
|
if (typeof colors !== "object" || colors === null) return 2;
|
|
@@ -51,6 +51,9 @@ jest.mock("../../../../../lib/paid-sections.generated", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
jest.mock("@working-theory/ui", () => ({
|
|
54
|
+
// #7789: renderPage's fallback page head renders inside the section Container.
|
|
55
|
+
Container: ({ children }: { children?: React.ReactNode }) =>
|
|
56
|
+
React.createElement("div", null, children),
|
|
54
57
|
// #7521 — the rest of the real barrel, so the listing extension finds
|
|
55
58
|
// `localeHref`; the two below still override.
|
|
56
59
|
...jest.requireActual("@working-theory/ui"),
|
|
@@ -24,6 +24,9 @@ jest.mock("next/navigation", () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
jest.mock("@working-theory/ui", () => ({
|
|
27
|
+
// #7789: renderPage's fallback page head renders inside the section Container.
|
|
28
|
+
Container: ({ children }: { children?: React.ReactNode }) =>
|
|
29
|
+
React.createElement("div", null, children),
|
|
27
30
|
Heading: ({
|
|
28
31
|
children,
|
|
29
32
|
level,
|
|
@@ -72,7 +72,7 @@ describe("careers/page — snapshot regression", () => {
|
|
|
72
72
|
});
|
|
73
73
|
const { container } = render(element);
|
|
74
74
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
75
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><h1>Careers at AILK</h1><p data-testid="careers-description">Join the team building the AX-first platform.</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">mdx-body-content</div></article></div></div></main>"`,
|
|
75
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Careers at AILK</h1><p data-testid="careers-description" class="mt-block-tight text-lead">Join the team building the AX-first platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-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 class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
78
|
});
|
|
@@ -97,7 +97,7 @@ describe("careers/[slug]/page — snapshot regression", () => {
|
|
|
97
97
|
});
|
|
98
98
|
const { container } = render(element);
|
|
99
99
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
100
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@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":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><h1>Senior Engineer</h1><p data-testid="senior-engineer-description">Build the AILK platform.</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">mdx-body-content</div></article></div></div></main>"`,
|
|
100
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@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":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Senior Engineer</h1><p data-testid="senior-engineer-description" class="mt-block-tight text-lead">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-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 class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
101
101
|
);
|
|
102
102
|
});
|
|
103
103
|
|
package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -68,7 +68,7 @@ describe("case-studies/page — snapshot regression", () => {
|
|
|
68
68
|
});
|
|
69
69
|
const { container } = render(element);
|
|
70
70
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
71
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><h1>Case Studies</h1><p data-testid="case-studies-description">Real-world accounts of teams using AI Launch Kit.</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">mdx-body-content</div></article></div></div></main>"`,
|
|
71
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-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 class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
@@ -93,7 +93,7 @@ describe("case-studies/[slug]/page — snapshot regression", () => {
|
|
|
93
93
|
});
|
|
94
94
|
const { container } = render(element);
|
|
95
95
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
96
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@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":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><h1>AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description">How a team shipped AEO with AI Launch Kit.</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">mdx-body-content</div></article></div></div></main>"`,
|
|
96
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@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":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-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 class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
|
|
97
97
|
);
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -16,6 +16,19 @@ import { render } from "@testing-library/react";
|
|
|
16
16
|
|
|
17
17
|
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
18
18
|
jest.mock("@working-theory/ui", () => ({
|
|
19
|
+
// #7745: renderPage's mdx-body branch frames its head and body in the
|
|
20
|
+
// section Container and renders the body through MDXContent + the shared map.
|
|
21
|
+
Container: ({
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}): React.ReactElement => React.createElement("div", null, children),
|
|
26
|
+
MDXContent: ({
|
|
27
|
+
children,
|
|
28
|
+
}: {
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}): React.ReactElement => React.createElement(React.Fragment, null, children),
|
|
31
|
+
getMDXComponents: (overrides?: Record<string, unknown>) => ({ ...overrides }),
|
|
19
32
|
// #4446/#4447: renderPage's own fallback/empty-state title now composes
|
|
20
33
|
// Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
|
|
21
34
|
Heading: ({
|
|
@@ -66,7 +79,7 @@ describe("ClientsPage — snapshot regression", () => {
|
|
|
66
79
|
});
|
|
67
80
|
const { container } = render(element);
|
|
68
81
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
69
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Our Clients","description":"Companies that trust us.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main id="main"><h1>Our Clients</h1><p data-testid="clients-description">Companies that trust us.</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-remote" data-source=""></div></article></div></div></main>"`,
|
|
82
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Our Clients","description":"Companies that trust us.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main id="main"><div><h1>Our Clients</h1><p data-testid="clients-description" class="mt-block-tight text-lead">Companies that trust us.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-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-remote" data-source=""></div></article></div></div></div></main>"`,
|
|
70
83
|
);
|
|
71
84
|
});
|
|
72
85
|
});
|
|
@@ -19,6 +19,9 @@ import { render, screen } from "@testing-library/react";
|
|
|
19
19
|
|
|
20
20
|
// ── @working-theory/ui mock ─────────────────────────────────────────────────
|
|
21
21
|
jest.mock("@working-theory/ui", () => ({
|
|
22
|
+
// #7789: renderPage's fallback page head renders inside the section Container.
|
|
23
|
+
Container: ({ children }: { children?: React.ReactNode }) =>
|
|
24
|
+
React.createElement("div", null, children),
|
|
22
25
|
// #4446/#4447: renderPage's own fallback/empty-state title composes Heading.
|
|
23
26
|
Heading: ({
|
|
24
27
|
children,
|
|
@@ -63,14 +66,22 @@ jest.mock("@working-theory/ui", () => ({
|
|
|
63
66
|
"data-fallback-email": props.fallbackEmail ?? "",
|
|
64
67
|
"data-terms-href": props.termsHref,
|
|
65
68
|
},
|
|
66
|
-
React.createElement(
|
|
69
|
+
React.createElement(
|
|
70
|
+
"button",
|
|
71
|
+
{ "data-testid": "contact-submit" },
|
|
72
|
+
props.submitLabel ?? "Send",
|
|
73
|
+
),
|
|
67
74
|
),
|
|
68
75
|
ContactFallback: (props: { email?: string; text?: { noEmail?: string } }) =>
|
|
69
76
|
React.createElement(
|
|
70
77
|
"div",
|
|
71
78
|
{ "data-testid": "contact-fallback" },
|
|
72
79
|
props.email
|
|
73
|
-
? React.createElement(
|
|
80
|
+
? React.createElement(
|
|
81
|
+
"a",
|
|
82
|
+
{ href: `mailto:${props.email}` },
|
|
83
|
+
props.email,
|
|
84
|
+
)
|
|
74
85
|
: props.text?.noEmail,
|
|
75
86
|
),
|
|
76
87
|
}));
|
|
@@ -111,8 +122,16 @@ import { readSiteContactEmail } from "../site-contact-email";
|
|
|
111
122
|
const mockCaptureStatus = leads.captureStatus as jest.Mock;
|
|
112
123
|
const mockContactEmail = readSiteContactEmail as jest.Mock;
|
|
113
124
|
|
|
114
|
-
const ACCEPTING = {
|
|
115
|
-
|
|
125
|
+
const ACCEPTING = {
|
|
126
|
+
ok: true,
|
|
127
|
+
status: 200,
|
|
128
|
+
body: { ok: true, data: { contact: true, newsletter: false } },
|
|
129
|
+
};
|
|
130
|
+
const REFUSING = {
|
|
131
|
+
ok: true,
|
|
132
|
+
status: 200,
|
|
133
|
+
body: { ok: true, data: { contact: false, newsletter: false } },
|
|
134
|
+
};
|
|
116
135
|
|
|
117
136
|
const source = {
|
|
118
137
|
frontmatter: {
|
|
@@ -124,7 +143,11 @@ const source = {
|
|
|
124
143
|
body: "",
|
|
125
144
|
};
|
|
126
145
|
|
|
127
|
-
const splitHero = {
|
|
146
|
+
const splitHero = {
|
|
147
|
+
id: "hero",
|
|
148
|
+
type: "hero",
|
|
149
|
+
data: { variant: "split", headline: "Hi" },
|
|
150
|
+
};
|
|
128
151
|
const contactFormSection = {
|
|
129
152
|
id: "contact-form",
|
|
130
153
|
type: "contact-form",
|
|
@@ -140,7 +163,9 @@ const contactFormSection = {
|
|
|
140
163
|
async function renderWith(sections: unknown[] | null) {
|
|
141
164
|
const element = await ContactPage({
|
|
142
165
|
params: Promise.resolve({ locale: "en" }),
|
|
143
|
-
getMdxSource: jest
|
|
166
|
+
getMdxSource: jest
|
|
167
|
+
.fn()
|
|
168
|
+
.mockResolvedValue(sections === null ? null : source),
|
|
144
169
|
getPageSections: jest.fn().mockResolvedValue(sections ?? []),
|
|
145
170
|
});
|
|
146
171
|
return render(element);
|
|
@@ -163,7 +188,7 @@ describe("ContactPage — snapshot regression", () => {
|
|
|
163
188
|
it("populated state renders deterministic HTML (renderPage sections + the form after it)", async () => {
|
|
164
189
|
const { container } = await renderWith([]);
|
|
165
190
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
166
|
-
`"<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="lead-capture-form" data-fields="" data-fallback-email="" data-terms-href="/en/terms"><button data-testid="contact-submit">Send</button></div>"`,
|
|
191
|
+
`"<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"><div><h1>Contact Us</h1><p data-testid="contact-description" class="mt-block-tight text-lead">Get in touch.</p></div><div data-testid="section-renderer" data-section-count="0"></div></main><div data-testid="lead-capture-form" data-fields="" data-fallback-email="" data-terms-href="/en/terms"><button data-testid="contact-submit">Send</button></div>"`,
|
|
167
192
|
);
|
|
168
193
|
});
|
|
169
194
|
});
|
|
@@ -184,7 +209,9 @@ describe("ContactPage — placement (#6730 D8)", () => {
|
|
|
184
209
|
const { container } = await renderWith([splitHero]);
|
|
185
210
|
|
|
186
211
|
expect(
|
|
187
|
-
container.querySelector(
|
|
212
|
+
container.querySelector(
|
|
213
|
+
"[data-section-id='hero'] [data-testid='lead-capture-form']",
|
|
214
|
+
),
|
|
188
215
|
).not.toBeNull();
|
|
189
216
|
expect(screen.getAllByTestId("lead-capture-form")).toHaveLength(1);
|
|
190
217
|
});
|
|
@@ -194,7 +221,9 @@ describe("ContactPage — placement (#6730 D8)", () => {
|
|
|
194
221
|
{ id: "faq", type: "faq", data: { items: [] } },
|
|
195
222
|
]);
|
|
196
223
|
|
|
197
|
-
expect(
|
|
224
|
+
expect(
|
|
225
|
+
container.querySelector("main [data-testid='lead-capture-form']"),
|
|
226
|
+
).toBeNull();
|
|
198
227
|
expect(screen.getAllByTestId("lead-capture-form")).toHaveLength(1);
|
|
199
228
|
});
|
|
200
229
|
|
|
@@ -216,7 +245,9 @@ describe("ContactPage — no lead destination (#6730 D8)", () => {
|
|
|
216
245
|
await renderWith([]);
|
|
217
246
|
|
|
218
247
|
const fallback = screen.getByTestId("contact-fallback");
|
|
219
|
-
expect(
|
|
248
|
+
expect(
|
|
249
|
+
fallback.querySelector("a[href='mailto:hi@example.com']"),
|
|
250
|
+
).not.toBeNull();
|
|
220
251
|
expect(screen.queryByTestId("contact-submit")).toBeNull();
|
|
221
252
|
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
222
253
|
});
|
|
@@ -239,14 +270,35 @@ describe("ContactPage — no lead destination (#6730 D8)", () => {
|
|
|
239
270
|
const { container } = await renderWith([contactFormSection]);
|
|
240
271
|
|
|
241
272
|
expect(
|
|
242
|
-
container.querySelector(
|
|
273
|
+
container.querySelector(
|
|
274
|
+
"[data-section-id='contact-form'] [data-testid='contact-fallback']",
|
|
275
|
+
),
|
|
243
276
|
).not.toBeNull();
|
|
244
277
|
});
|
|
245
278
|
|
|
246
279
|
it.each([
|
|
247
|
-
[
|
|
248
|
-
|
|
249
|
-
|
|
280
|
+
[
|
|
281
|
+
"rejects (unreachable API)",
|
|
282
|
+
() => mockCaptureStatus.mockRejectedValue(new Error("ECONNREFUSED")),
|
|
283
|
+
],
|
|
284
|
+
[
|
|
285
|
+
"404 (older API)",
|
|
286
|
+
() =>
|
|
287
|
+
mockCaptureStatus.mockResolvedValue({
|
|
288
|
+
ok: false,
|
|
289
|
+
status: 404,
|
|
290
|
+
body: {},
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
"5xx",
|
|
295
|
+
() =>
|
|
296
|
+
mockCaptureStatus.mockResolvedValue({
|
|
297
|
+
ok: false,
|
|
298
|
+
status: 503,
|
|
299
|
+
body: {},
|
|
300
|
+
}),
|
|
301
|
+
],
|
|
250
302
|
])("status %s → the form renders", async (_label, arrange) => {
|
|
251
303
|
arrange();
|
|
252
304
|
|