create-ailk 0.18.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- 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 +17 -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__/deliverable-asset-store.test.ts +38 -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/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- 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 +19 -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 +218 -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 +173 -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 +135 -14
- 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/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- 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 +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- 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 +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- 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/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- 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__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -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/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- 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 +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -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
|
+
});
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the account layout — the base area's SignedInShell frame
|
|
3
|
-
* (#7430, spec 7423 U4 · D13; the U4 half of AC-17).
|
|
3
|
+
* (#7430, spec 7423 U4 · D13; the U4 half of AC-17; #8231 D1).
|
|
4
4
|
*
|
|
5
5
|
* Covers:
|
|
6
|
-
* 1. The rail: the site logo as the top tile (a link home), Inbox
|
|
7
|
-
*
|
|
6
|
+
* 1. The rail: the site logo as the top tile (a link home), then Inbox and
|
|
7
|
+
* a Library group holding Artifacts — each selected on its own paths,
|
|
8
|
+
* no row on a Settings page — and a footer of the avatar and ? only.
|
|
9
|
+
* No Settings row (#8334: Settings lives in the avatar menu, as on the
|
|
10
|
+
* Admin).
|
|
8
11
|
* 2. The avatar opens the card menu (name, then Settings and Log out side
|
|
9
|
-
* by side);
|
|
12
|
+
* by side); Settings is the one route to /account/general; Log out
|
|
13
|
+
* signs out and routes home.
|
|
10
14
|
* 3. ? opens Contact support (→ /account/support) and, only when the site
|
|
11
15
|
* configures a help URL, Help center.
|
|
12
16
|
* 4. Below md the tab bar reads avatar · Inbox · For you · Help, with no
|
|
@@ -15,9 +19,16 @@
|
|
|
15
19
|
* tab to the page (PhoneTabProvider).
|
|
16
20
|
* 5. The For-you items are the aside; none → no aside.
|
|
17
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.
|
|
18
29
|
*/
|
|
19
30
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
20
|
-
import type { ReactElement } from "react";
|
|
31
|
+
import type { ReactElement, ReactNode } from "react";
|
|
21
32
|
|
|
22
33
|
jest.mock("next-intl/server", () => {
|
|
23
34
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
@@ -55,13 +66,26 @@ jest.mock("../../../../../auth", () => ({
|
|
|
55
66
|
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
56
67
|
}));
|
|
57
68
|
|
|
58
|
-
const mockCheckoutsMe = jest.fn();
|
|
59
69
|
const mockSubscriptionMe = jest.fn();
|
|
70
|
+
// #8231 D7 — the member reads call on every topology; here every one 404s.
|
|
71
|
+
const mockMemberNotFound = jest.fn();
|
|
60
72
|
jest.mock("@working-theory/api-client", () => ({
|
|
61
73
|
__esModule: true,
|
|
62
74
|
waitlistSignups: { me: jest.fn() },
|
|
63
|
-
flowCheckouts: { me: (
|
|
64
|
-
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() },
|
|
78
|
+
member: {
|
|
79
|
+
pieces: (...args: unknown[]) => mockMemberNotFound(...args),
|
|
80
|
+
messages: (...args: unknown[]) => mockMemberNotFound(...args),
|
|
81
|
+
purchases: (...args: unknown[]) => mockMemberNotFound(...args),
|
|
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),
|
|
86
|
+
},
|
|
87
|
+
hubSignIn: { me: jest.fn() },
|
|
88
|
+
visitorForwardHeaders: () => null,
|
|
65
89
|
}));
|
|
66
90
|
|
|
67
91
|
const mockSignOut = jest.fn();
|
|
@@ -76,7 +100,8 @@ let mockPathname = "/account";
|
|
|
76
100
|
let mockSearch = "";
|
|
77
101
|
jest.mock("next/navigation", () => ({
|
|
78
102
|
__esModule: true,
|
|
79
|
-
useRouter: () => ({ push: mockPush, refresh: mockRefresh }),
|
|
103
|
+
useRouter: () => ({ push: mockPush, refresh: mockRefresh, replace: jest.fn() }),
|
|
104
|
+
redirect: jest.fn(),
|
|
80
105
|
usePathname: () => mockPathname,
|
|
81
106
|
useSearchParams: () => new URLSearchParams(mockSearch),
|
|
82
107
|
}));
|
|
@@ -84,6 +109,8 @@ jest.mock("next/navigation", () => ({
|
|
|
84
109
|
import { usePhoneTab } from "@working-theory/templates/account";
|
|
85
110
|
|
|
86
111
|
import AccountLayout from "../layout";
|
|
112
|
+
import InboxPage from "../page";
|
|
113
|
+
import { SETTINGS_ROUTES, type SettingsTabKey } from "../settings-frame";
|
|
87
114
|
|
|
88
115
|
/** A page that shows the phone tab the frame hands it. */
|
|
89
116
|
function PhoneTabProbe(): ReactElement {
|
|
@@ -118,16 +145,16 @@ function openByPointer(trigger: HTMLElement): void {
|
|
|
118
145
|
fireEvent.pointerDown(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
|
|
119
146
|
}
|
|
120
147
|
|
|
121
|
-
async function renderLayout(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
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 }) });
|
|
131
158
|
render(element);
|
|
132
159
|
}
|
|
133
160
|
|
|
@@ -139,8 +166,8 @@ beforeEach(() => {
|
|
|
139
166
|
mockSearch = "";
|
|
140
167
|
delete process.env.AILK_HELP_CENTER_URL;
|
|
141
168
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
142
|
-
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [] });
|
|
143
169
|
mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
|
|
170
|
+
mockMemberNotFound.mockReset().mockResolvedValue({ ok: false, status: 404, body: null });
|
|
144
171
|
mockSignOut.mockReset().mockResolvedValue({ data: {}, error: null });
|
|
145
172
|
mockPush.mockReset();
|
|
146
173
|
mockRefresh.mockReset();
|
|
@@ -167,16 +194,68 @@ describe("the account rail (D13, AC-17)", () => {
|
|
|
167
194
|
expect(within(rail).getByTestId("account-site-logo")).toHaveAttribute("href", "/fr/");
|
|
168
195
|
});
|
|
169
196
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const links = within(rail)
|
|
197
|
+
/** The rail's rows, in DOM order — every link but the logo. */
|
|
198
|
+
function railRows(): HTMLElement[] {
|
|
199
|
+
return within(screen.getByTestId("sidebar-desktop"))
|
|
174
200
|
.getAllByRole("link")
|
|
175
201
|
.filter((link) => link.getAttribute("data-testid") !== "account-site-logo");
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
it("the rows are Inbox, then Artifacts under Library — no Settings row (#8231 D1, #8334)", async () => {
|
|
205
|
+
await renderLayout();
|
|
206
|
+
const rows = railRows();
|
|
207
|
+
expect(rows.map((row) => [row.textContent, row.getAttribute("href")])).toEqual([
|
|
208
|
+
["Inbox", "/account"],
|
|
209
|
+
["Artifacts", "/account/artifacts"],
|
|
210
|
+
]);
|
|
211
|
+
expect(rows[0]).toHaveAttribute("aria-current", "page");
|
|
212
|
+
// Artifacts sits in the Library group.
|
|
213
|
+
const heading = screen.getByTestId("sidebar-group-heading-library");
|
|
214
|
+
expect(heading).toHaveTextContent("Library");
|
|
215
|
+
const library = heading.parentElement as HTMLElement;
|
|
216
|
+
expect(within(library).getByText("Artifacts")).toBeInTheDocument();
|
|
217
|
+
// #8334 — Settings is nowhere in the rail, at any viewport: not a row, not
|
|
218
|
+
// a tab. The avatar menu is its one route (the next describe).
|
|
219
|
+
expect(within(screen.getByTestId("sidebar-desktop")).queryByText("Settings")).not.toBeInTheDocument();
|
|
220
|
+
expect(within(screen.getByTestId("bottom-tab-bar")).queryByText("Settings")).not.toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it.each([
|
|
224
|
+
["/account", "Inbox"],
|
|
225
|
+
["/account/inbox/purchase/x", "Inbox"],
|
|
226
|
+
["/account/artifacts", "Artifacts"],
|
|
227
|
+
["/account/artifacts/p1", "Artifacts"],
|
|
228
|
+
])("on %s only %s is selected (#8231 D1)", async (path, selected) => {
|
|
229
|
+
mockPathname = path;
|
|
230
|
+
await renderLayout();
|
|
231
|
+
const current = railRows().filter((row) => row.getAttribute("aria-current") === "page");
|
|
232
|
+
expect(current.map((row) => row.textContent)).toEqual([selected]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
"/account/support",
|
|
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).
|
|
240
|
+
"/account/settings",
|
|
241
|
+
"/fr/account/support",
|
|
242
|
+
])("no row is selected on %s (#8231 D1, #8334)", async (path) => {
|
|
243
|
+
mockPathname = path;
|
|
244
|
+
await renderLayout(path.startsWith("/fr/") ? "fr" : "en");
|
|
245
|
+
const rows = railRows();
|
|
246
|
+
expect(rows).toHaveLength(2);
|
|
247
|
+
expect(rows.some((row) => row.hasAttribute("aria-current"))).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("a non-default locale prefixes every row (#8231 D1)", async () => {
|
|
251
|
+
mockPathname = "/fr/account/artifacts";
|
|
252
|
+
await renderLayout("fr");
|
|
253
|
+
const rows = railRows();
|
|
254
|
+
expect(rows.map((row) => row.getAttribute("href"))).toEqual([
|
|
255
|
+
"/fr/account",
|
|
256
|
+
"/fr/account/artifacts",
|
|
257
|
+
]);
|
|
258
|
+
expect(rows[1]).toHaveAttribute("aria-current", "page");
|
|
180
259
|
});
|
|
181
260
|
|
|
182
261
|
it("an Inbox item page keeps Inbox selected", async () => {
|
|
@@ -215,11 +294,28 @@ describe("the avatar menu (D13)", () => {
|
|
|
215
294
|
const actions = within(menu).getByTestId("account-menu-actions");
|
|
216
295
|
expect(within(actions).getByText("Settings").closest("a")).toHaveAttribute(
|
|
217
296
|
"href",
|
|
218
|
-
"/account/
|
|
297
|
+
"/account/general",
|
|
219
298
|
);
|
|
220
299
|
expect(within(actions).getByTestId("account-menu-sign-out")).toHaveTextContent("Log out");
|
|
221
300
|
});
|
|
222
301
|
|
|
302
|
+
it("Settings in the menu is the one route to General, locale-prefixed (#8334)", async () => {
|
|
303
|
+
mockPathname = "/fr/account";
|
|
304
|
+
await renderLayout("fr");
|
|
305
|
+
openByPointer(screen.getByTestId("sidebar-nav-user-trigger"));
|
|
306
|
+
const actions = within(screen.getByTestId("account-menu-content")).getByTestId(
|
|
307
|
+
"account-menu-actions",
|
|
308
|
+
);
|
|
309
|
+
expect(within(actions).getByText("Settings").closest("a")).toHaveAttribute(
|
|
310
|
+
"href",
|
|
311
|
+
"/fr/account/general",
|
|
312
|
+
);
|
|
313
|
+
// The rail offers no second Settings link.
|
|
314
|
+
expect(
|
|
315
|
+
within(screen.getByTestId("sidebar-desktop")).queryByRole("link", { name: "Settings" }),
|
|
316
|
+
).not.toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
223
319
|
it("Log out signs out, then routes home and refreshes", async () => {
|
|
224
320
|
await renderLayout();
|
|
225
321
|
openByPointer(screen.getByTestId("sidebar-nav-user-trigger"));
|
|
@@ -370,3 +466,110 @@ describe("no session", () => {
|
|
|
370
466
|
expect(screen.queryByTestId("sidebar-desktop")).not.toBeInTheDocument();
|
|
371
467
|
});
|
|
372
468
|
});
|
|
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
|
+
|
|
558
|
+
describe("the shell marker (#8337 D4, AC-6)", () => {
|
|
559
|
+
it("with a session, exactly one [data-ailk-shell=\"account\"] wraps the shell", async () => {
|
|
560
|
+
await renderLayout();
|
|
561
|
+
const markers = document.querySelectorAll("[data-ailk-shell]");
|
|
562
|
+
expect(markers).toHaveLength(1);
|
|
563
|
+
const marker = markers[0] as HTMLElement;
|
|
564
|
+
expect(marker.getAttribute("data-ailk-shell")).toBe("account");
|
|
565
|
+
expect(marker.getAttribute("data-testid")).toBe("account-shell");
|
|
566
|
+
expect(Array.from(marker.classList)).toEqual(["contents"]);
|
|
567
|
+
expect(within(marker).getByTestId("dashboard-shell")).toBeInTheDocument();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("with no session, no [data-ailk-shell] exists", async () => {
|
|
571
|
+
mockGetSession.mockResolvedValue(null);
|
|
572
|
+
await renderLayout();
|
|
573
|
+
expect(document.querySelector("[data-ailk-shell]")).not.toBeInTheDocument();
|
|
574
|
+
});
|
|
575
|
+
});
|