create-ailk 0.19.0 → 0.19.1

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.
Files changed (91) hide show
  1. package/dist/add-module.js +7 -2
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/lib/scaffold-render-sweep.d.ts +23 -0
  5. package/dist/lib/scaffold-render-sweep.js +52 -0
  6. package/dist/module-architecture.js +9 -3
  7. package/dist/programmatic.js +8 -1
  8. package/dist/site-roster.js +21 -4
  9. package/dist/surfaces.d.ts +14 -0
  10. package/dist/surfaces.js +31 -1
  11. package/dist/topology.js +29 -1
  12. package/package.json +3 -3
  13. package/templates/apps/api/.env.example +17 -1
  14. package/templates/apps/api/CLAUDE.md +10 -4
  15. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  16. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  17. package/templates/apps/api/src/lib/__mocks__/prisma.ts +8 -0
  18. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  19. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  20. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  21. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  22. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  23. package/templates/apps/api/src/lib/stripe.ts +97 -5
  24. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +10 -4
  25. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  26. package/templates/apps/api/src/middleware/auth.ts +11 -3
  27. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  28. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  29. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  30. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  31. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  32. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  33. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  34. package/templates/apps/api/src/routes/leads/index.ts +15 -0
  35. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  36. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  37. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  38. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  39. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  40. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  41. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  42. package/templates/apps/api/src/server.ts +111 -16
  43. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  44. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  45. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +115 -4
  46. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  47. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  48. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  49. package/templates/apps/api/src/services/consent.ts +488 -3
  50. package/templates/apps/api/src/services/lead-routing.ts +105 -12
  51. package/templates/apps/api/src/services/person.ts +501 -1
  52. package/templates/apps/api/src/services/project-listings.ts +7 -2
  53. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  54. package/templates/apps/api/src/services/revalidation.ts +11 -1
  55. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  56. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +17 -3
  57. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +156 -0
  58. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +89 -14
  59. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  60. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +122 -18
  61. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +15 -1
  62. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +290 -20
  63. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +55 -0
  64. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +74 -4
  65. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +4 -7
  66. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +3 -3
  67. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  68. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +66 -4
  69. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +26 -3
  70. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +2 -2
  71. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +89 -37
  72. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  73. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  74. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +41 -33
  75. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +1 -1
  76. package/templates/apps/web/auth.ts +20 -1
  77. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  78. package/templates/apps/web/lib/auth-email.ts +45 -10
  79. package/templates/apps/web/lib/topology.generated.ts +9 -0
  80. package/templates/apps/web/messages/en.json +54 -14
  81. package/templates/database/CHANGELOG.md +16 -0
  82. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  83. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  84. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  85. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  86. package/templates/database/__tests__/person.db.test.ts +3 -1
  87. package/templates/database/inbox/schema.prisma +76 -10
  88. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  89. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  90. package/templates/database/package.json +1 -1
  91. package/versions.json +35 -35
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The account layout hands the generated seam's sign-out wiring to the client
3
+ * frame (#8465): a client component cannot import `topology.generated.ts`, so
4
+ * the server layout reads `SIGN_OUT_ENDPOINT` / `SIGN_IN_PATH` and passes them
5
+ * as props — the signed-in Nav's own pattern. Also pins that the Help center
6
+ * href is rendered only for an https URL (the #8467 review's Low finding).
7
+ */
8
+ import { render } from "@testing-library/react";
9
+ import type { ReactElement } from "react";
10
+
11
+ jest.mock("next-intl/server", () => ({
12
+ __esModule: true,
13
+ getTranslations: async (arg: { namespace?: string }) => (key: string) =>
14
+ `${arg.namespace}.${key}`,
15
+ }));
16
+
17
+ let mockSeam: {
18
+ SIGN_IN_PATH: string | null;
19
+ SIGN_OUT_ENDPOINT: string | null;
20
+ } = {
21
+ SIGN_IN_PATH: "/sign-in",
22
+ SIGN_OUT_ENDPOINT: null,
23
+ };
24
+ // eslint-disable-next-line no-restricted-syntax -- #507: the seam is generated per build (#7894 D8); its `signIn: "hub"` render is the boundary under test, and no env or HTTP surface selects it.
25
+ jest.mock("../../../../../lib/topology.generated", () => ({
26
+ __esModule: true,
27
+ get SIGN_IN_PATH() {
28
+ return mockSeam.SIGN_IN_PATH;
29
+ },
30
+ get SIGN_OUT_ENDPOINT() {
31
+ return mockSeam.SIGN_OUT_ENDPOINT;
32
+ },
33
+ }));
34
+
35
+ // eslint-disable-next-line no-restricted-syntax -- #507: test-seam mock; the layout under test composes these account modules and the suite asserts only the sign-out endpoint it forwards
36
+ jest.mock("../account-data", () => ({
37
+ __esModule: true,
38
+ accountSession: async () => ({
39
+ ctx: { user: { name: "Ada", email: "ada@example.com" }, t: {} },
40
+ }),
41
+ loadForYou: async () => ({ items: [], now: "2026-10-12T00:00:00.000Z" }),
42
+ }));
43
+
44
+ // eslint-disable-next-line no-restricted-syntax -- #507: test-seam mock; the layout under test composes these account modules and the suite asserts only the sign-out endpoint it forwards
45
+ jest.mock("../for-you-labels", () => ({
46
+ __esModule: true,
47
+ forYouLabels: () => ({}),
48
+ }));
49
+ // eslint-disable-next-line no-restricted-syntax -- #507: test-seam mock; the layout under test composes these account modules and the suite asserts only the sign-out endpoint it forwards
50
+ jest.mock("../../../../../lib/site-logo", () => ({
51
+ __esModule: true,
52
+ siteLogo: () => null,
53
+ }));
54
+
55
+ const mockFrame = jest.fn();
56
+ // eslint-disable-next-line no-restricted-syntax -- #507: test-seam mock; the layout under test composes these account modules and the suite asserts only the sign-out endpoint it forwards
57
+ jest.mock("../account-frame", () => ({
58
+ __esModule: true,
59
+ AccountFrame: (props: Record<string, unknown>) => {
60
+ mockFrame(props);
61
+ return null;
62
+ },
63
+ }));
64
+
65
+ import AccountLayout from "../layout";
66
+
67
+ const ENV = process.env;
68
+
69
+ async function renderLayout(locale = "en"): Promise<Record<string, unknown>> {
70
+ const node = (await AccountLayout({
71
+ children: null,
72
+ params: Promise.resolve({ locale }),
73
+ })) as ReactElement;
74
+ render(node);
75
+ return mockFrame.mock.calls[0]![0] as Record<string, unknown>;
76
+ }
77
+
78
+ beforeEach(() => {
79
+ mockFrame.mockReset();
80
+ mockSeam = { SIGN_IN_PATH: "/sign-in", SIGN_OUT_ENDPOINT: null };
81
+ process.env = { ...ENV };
82
+ delete process.env.AILK_HELP_CENTER_URL;
83
+ });
84
+
85
+ afterEach(() => {
86
+ process.env = ENV;
87
+ });
88
+
89
+ describe("sign-out wiring (#8465)", () => {
90
+ it("passes the endpoint and the localized sign-in path on a hub sign-in spoke", async () => {
91
+ mockSeam = {
92
+ SIGN_IN_PATH: "/account",
93
+ SIGN_OUT_ENDPOINT: "/api/hub-session/sign-out",
94
+ };
95
+ const props = await renderLayout("fr");
96
+ expect(props.signOutEndpoint).toBe("/api/hub-session/sign-out");
97
+ expect(props.signInHref).toBe("/fr/account");
98
+ });
99
+
100
+ it("falls back to the home when the seam names no sign-in path", async () => {
101
+ mockSeam = {
102
+ SIGN_IN_PATH: null,
103
+ SIGN_OUT_ENDPOINT: "/api/hub-session/sign-out",
104
+ };
105
+ expect((await renderLayout()).signInHref).toBe("/");
106
+ });
107
+
108
+ it("passes neither on a hub or a single site (no endpoint)", async () => {
109
+ const props = await renderLayout();
110
+ expect(props).not.toHaveProperty("signOutEndpoint");
111
+ expect(props).not.toHaveProperty("signInHref");
112
+ });
113
+
114
+ it("carries the failed-sign-out label to the frame", async () => {
115
+ const props = await renderLayout();
116
+ expect((props.labels as Record<string, string>).signOutFailed).toBe(
117
+ "account.frame.signOutFailed",
118
+ );
119
+ });
120
+ });
121
+
122
+ describe("Help center href (#8467 review, Low)", () => {
123
+ const helpItems = (
124
+ props: Record<string, unknown>,
125
+ ): { id: string; href: string }[] =>
126
+ props.helpItems as { id: string; href: string }[];
127
+
128
+ it("renders an https URL", async () => {
129
+ process.env.AILK_HELP_CENTER_URL = "https://help.example.com";
130
+ expect(helpItems(await renderLayout()).map((i) => i.id)).toContain(
131
+ "help-center",
132
+ );
133
+ });
134
+
135
+ it.each([
136
+ "javascript:alert(1)",
137
+ "http://help.example.com",
138
+ "not a url",
139
+ "data:text/html,x",
140
+ ])("drops %s", async (value) => {
141
+ process.env.AILK_HELP_CENTER_URL = value;
142
+ expect(helpItems(await renderLayout()).map((i) => i.id)).not.toContain(
143
+ "help-center",
144
+ );
145
+ });
146
+ });
@@ -19,9 +19,16 @@
19
19
  * tab to the page (PhoneTabProvider).
20
20
  * 5. The For-you items are the aside; none → no aside.
21
21
  * 6. No site chrome (Nav, Footer, ThemeToggle) inside the frame.
22
+ * 7. The four Settings pages render alone, inside the shell marker only —
23
+ * no rail, no Inbox, no AppShell frame (#8430): the page draws its own
24
+ * standalone settings frame, as the Admin's does. Membership is an
25
+ * exact, locale-aware match on settings-frame.tsx's `SETTINGS_ROUTES`
26
+ * (#8441): a sub-route of a settings path stays inside the shell.
27
+ * 8. One main landmark (#8441): every settings route and the Inbox, each
28
+ * rendered through this layout with its real page, draw exactly one.
22
29
  */
23
30
  import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
24
- import type { ReactElement } from "react";
31
+ import type { ReactElement, ReactNode } from "react";
25
32
 
26
33
  jest.mock("next-intl/server", () => {
27
34
  const msgs: unknown = require("../../../../../messages/en.json");
@@ -65,13 +72,20 @@ const mockMemberNotFound = jest.fn();
65
72
  jest.mock("@working-theory/api-client", () => ({
66
73
  __esModule: true,
67
74
  waitlistSignups: { me: jest.fn() },
68
- subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
75
+ flowCheckouts: { me: jest.fn() },
76
+ subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args), portalSession: jest.fn() },
77
+ leads: { submit: jest.fn() },
69
78
  member: {
70
79
  pieces: (...args: unknown[]) => mockMemberNotFound(...args),
71
80
  messages: (...args: unknown[]) => mockMemberNotFound(...args),
72
81
  purchases: (...args: unknown[]) => mockMemberNotFound(...args),
73
82
  appointments: (...args: unknown[]) => mockMemberNotFound(...args),
83
+ // #8441 — the Inbox page's New-menu availability reads, for § one main landmark.
84
+ meetingTypes: (...args: unknown[]) => mockMemberNotFound(...args),
85
+ requestForm: (...args: unknown[]) => mockMemberNotFound(...args),
74
86
  },
87
+ hubSignIn: { me: jest.fn() },
88
+ visitorForwardHeaders: () => null,
75
89
  }));
76
90
 
77
91
  const mockSignOut = jest.fn();
@@ -86,7 +100,8 @@ let mockPathname = "/account";
86
100
  let mockSearch = "";
87
101
  jest.mock("next/navigation", () => ({
88
102
  __esModule: true,
89
- useRouter: () => ({ push: mockPush, refresh: mockRefresh }),
103
+ useRouter: () => ({ push: mockPush, refresh: mockRefresh, replace: jest.fn() }),
104
+ redirect: jest.fn(),
90
105
  usePathname: () => mockPathname,
91
106
  useSearchParams: () => new URLSearchParams(mockSearch),
92
107
  }));
@@ -94,6 +109,8 @@ jest.mock("next/navigation", () => ({
94
109
  import { usePhoneTab } from "@working-theory/templates/account";
95
110
 
96
111
  import AccountLayout from "../layout";
112
+ import InboxPage from "../page";
113
+ import { SETTINGS_ROUTES, type SettingsTabKey } from "../settings-frame";
97
114
 
98
115
  /** A page that shows the phone tab the frame hands it. */
99
116
  function PhoneTabProbe(): ReactElement {
@@ -128,16 +145,16 @@ function openByPointer(trigger: HTMLElement): void {
128
145
  fireEvent.pointerDown(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
129
146
  }
130
147
 
131
- async function renderLayout(locale = "en"): Promise<void> {
132
- const element = await AccountLayout({
133
- children: (
134
- <>
135
- <p data-testid="account-child">page</p>
136
- <PhoneTabProbe />
137
- </>
138
- ),
139
- params: Promise.resolve({ locale }),
140
- });
148
+ async function renderLayout(
149
+ locale = "en",
150
+ children: ReactNode = (
151
+ <>
152
+ <p data-testid="account-child">page</p>
153
+ <PhoneTabProbe />
154
+ </>
155
+ ),
156
+ ): Promise<void> {
157
+ const element = await AccountLayout({ children, params: Promise.resolve({ locale }) });
141
158
  render(element);
142
159
  }
143
160
 
@@ -217,12 +234,11 @@ describe("the account rail (D13, AC-17)", () => {
217
234
 
218
235
  it.each([
219
236
  "/account/support",
220
- // #8334 — the Settings pages (and the old path that redirects to General)
221
- // select no row: Settings has no rail row to select.
222
- "/account/general",
223
- "/account/billing",
237
+ // #8334 — the old Settings path (it redirects to General) selects no
238
+ // row: Settings has no rail row to select. The four Settings pages render
239
+ // no rail at all (#8430, § the settings routes below).
224
240
  "/account/settings",
225
- "/fr/account/billing",
241
+ "/fr/account/support",
226
242
  ])("no row is selected on %s (#8231 D1, #8334)", async (path) => {
227
243
  mockPathname = path;
228
244
  await renderLayout(path.startsWith("/fr/") ? "fr" : "en");
@@ -451,6 +467,94 @@ describe("no session", () => {
451
467
  });
452
468
  });
453
469
 
470
+ describe("the settings routes take the screen (#8430)", () => {
471
+ it.each([
472
+ ["/account/general", "en"],
473
+ ["/account/security", "en"],
474
+ ["/account/notifications", "en"],
475
+ ["/account/billing", "en"],
476
+ ["/fr/account/billing", "fr"],
477
+ ])("on %s the page renders alone — no rail, no Inbox, no AppShell frame", async (path, locale) => {
478
+ mockPathname = path;
479
+ await renderLayout(locale);
480
+ expect(screen.getByTestId("account-child")).toBeInTheDocument();
481
+ expect(screen.queryByTestId("dashboard-shell")).not.toBeInTheDocument();
482
+ expect(screen.queryByTestId("sidebar-desktop")).not.toBeInTheDocument();
483
+ expect(screen.queryByTestId("app-shell-aside")).not.toBeInTheDocument();
484
+ expect(screen.queryByRole("link", { name: "Inbox" })).not.toBeInTheDocument();
485
+ expect(screen.queryByRole("link", { name: "Artifacts" })).not.toBeInTheDocument();
486
+ });
487
+
488
+ it("keeps the shell marker around the page, so the Admin's tokens still apply (#8337 D4)", async () => {
489
+ mockPathname = "/account/general";
490
+ await renderLayout();
491
+ const markers = document.querySelectorAll("[data-ailk-shell]");
492
+ expect(markers).toHaveLength(1);
493
+ const marker = markers[0] as HTMLElement;
494
+ expect(marker.getAttribute("data-ailk-shell")).toBe("account");
495
+ expect(Array.from(marker.classList)).toEqual(["contents"]);
496
+ expect(within(marker).getByTestId("account-child")).toBeInTheDocument();
497
+ });
498
+
499
+ it.each([
500
+ "/account",
501
+ "/account/artifacts",
502
+ "/account/support",
503
+ "/account/inbox/purchase/fc_1",
504
+ // #8441 — the match is exact: a sub-route of a settings path is not listed,
505
+ // so it stays inside the shell.
506
+ "/account/billing/history",
507
+ "/account/general/",
508
+ "/fr/account/security/sessions",
509
+ ])(
510
+ "%s still renders inside the frame",
511
+ async (path) => {
512
+ mockPathname = path;
513
+ await renderLayout(path.startsWith("/fr/") ? "fr" : "en");
514
+ expect(screen.getByTestId("dashboard-shell")).toBeInTheDocument();
515
+ expect(within(screen.getByTestId("sidebar-desktop")).getByRole("link", { name: "Inbox" })).toBeInTheDocument();
516
+ },
517
+ );
518
+ });
519
+
520
+ describe("one main landmark (#8441)", () => {
521
+ type Page = (props: { params: Promise<{ locale: string }> }) => Promise<ReactElement>;
522
+ // Keyed by the list's own type, so a tab added to `SETTINGS_ROUTES` without
523
+ // its page here fails to compile. Billing loads with `import()`:
524
+ // architecture.yaml bans a static import of a relative billing/ path
525
+ // (billing.test.tsx).
526
+ const PAGES: Record<SettingsTabKey, () => Promise<Page>> = {
527
+ general: async () => (await import("../general/page")).default,
528
+ security: async () => (await import("../security/page")).default,
529
+ notifications: async () => (await import("../notifications/page")).default,
530
+ billing: async () => (await import("../billing/page")).default,
531
+ };
532
+
533
+ async function renderRoute(path: string, locale: string, page: Page): Promise<void> {
534
+ mockPathname = path;
535
+ await renderLayout(locale, await page({ params: Promise.resolve({ locale }) }));
536
+ }
537
+
538
+ it.each(SETTINGS_ROUTES.flatMap(({ key, path }) => [
539
+ [path, "en", key] as const,
540
+ [`/fr${path}`, "fr", key] as const,
541
+ ]))("%s draws exactly one <main>", async (path, locale, key) => {
542
+ await renderRoute(path, locale, await PAGES[key]());
543
+ expect(screen.getAllByRole("main")).toHaveLength(1);
544
+ expect(document.querySelectorAll("main")).toHaveLength(1);
545
+ });
546
+
547
+ it.each([
548
+ ["/account", "en"],
549
+ ["/fr/account", "fr"],
550
+ ])("the Inbox, %s, draws exactly one <main>", async (path, locale) => {
551
+ await renderRoute(path, locale, InboxPage);
552
+ expect(screen.getByTestId("dashboard-shell")).toBeInTheDocument();
553
+ expect(screen.getAllByRole("main")).toHaveLength(1);
554
+ expect(document.querySelectorAll("main")).toHaveLength(1);
555
+ });
556
+ });
557
+
454
558
  describe("the shell marker (#8337 D4, AC-6)", () => {
455
559
  it("with a session, exactly one [data-ailk-shell=\"account\"] wraps the shell", async () => {
456
560
  await renderLayout();
@@ -7,7 +7,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
 
10
- import { render, screen } from "@testing-library/react";
10
+ import { render, screen, within } from "@testing-library/react";
11
11
 
12
12
  jest.mock("next-intl/server", () => {
13
13
  const msgs: unknown = require("../../../../../messages/en.json");
@@ -102,6 +102,20 @@ describe("the frame (D7, D8)", () => {
102
102
  });
103
103
  });
104
104
 
105
+ describe("the standalone shape (#8430)", () => {
106
+ it("takes the screen as the Admin's settings do: the rail column and the pane are the page's own landmarks", async () => {
107
+ await renderPage();
108
+ const settings = screen.getByTestId("account-settings");
109
+ expect(settings).toHaveClass("min-h-screen");
110
+ const column = within(settings).getByRole("complementary");
111
+ expect(column.firstElementChild).toBe(screen.getByTestId("account-settings-back"));
112
+ expect(column).toContainElement(screen.getByTestId("account-settings-rail"));
113
+ expect(within(settings).getByRole("main")).toContainElement(
114
+ screen.getByRole("heading", { level: 1 }),
115
+ );
116
+ });
117
+ });
118
+
105
119
  describe("cookie settings (D7, moved from general.test.tsx)", () => {
106
120
  it("links to /privacy#cookie-settings", async () => {
107
121
  await renderPage();