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.
- package/dist/add-module.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/scaffold-render-sweep.d.ts +23 -0
- package/dist/lib/scaffold-render-sweep.js +52 -0
- package/dist/module-architecture.js +9 -3
- package/dist/programmatic.js +8 -1
- package/dist/site-roster.js +21 -4
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +31 -1
- package/dist/topology.js +29 -1
- package/package.json +3 -3
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +10 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +8 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/stripe.ts +97 -5
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +10 -4
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +11 -3
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +15 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +111 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +115 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +105 -12
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +17 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +156 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +89 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +122 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +15 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +290 -20
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +55 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +74 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +4 -7
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +66 -4
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +26 -3
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +2 -2
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +89 -37
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +41 -33
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +1 -1
- package/templates/apps/web/auth.ts +20 -1
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/topology.generated.ts +9 -0
- package/templates/apps/web/messages/en.json +54 -14
- package/templates/database/CHANGELOG.md +16 -0
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/inbox/schema.prisma +76 -10
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +1 -1
- 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
|
-
|
|
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(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
221
|
-
//
|
|
222
|
-
|
|
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/
|
|
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();
|