create-ailk 0.19.1 → 0.21.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 +84 -7
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/module-architecture.js +55 -2
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +76 -22
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +78 -2
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +53 -0
- package/dist/update.js +606 -0
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/README.md +26 -25
- package/templates/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +18 -10
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
- package/templates/apps/api/src/middleware/auth.ts +8 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
- package/templates/apps/api/src/server.ts +14 -2
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- package/templates/apps/api/src/services/audit-log.ts +72 -10
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
- package/templates/apps/api/vercel.json +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- 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]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +46 -4
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- 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 +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +4 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +83 -9
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +48 -0
- package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/project.yaml +6 -3
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
|
@@ -126,7 +126,7 @@ import { seal } from "../../../../../lib/hub-session/seal";
|
|
|
126
126
|
import { BillingAnchor } from "../general/billing-anchor";
|
|
127
127
|
import { saveContact } from "../general/contact-actions";
|
|
128
128
|
import Page from "../general/page";
|
|
129
|
-
import {
|
|
129
|
+
import { saveProfile } from "../profile-actions";
|
|
130
130
|
|
|
131
131
|
const PHOTO = "https://cdn.example.com/ada.png";
|
|
132
132
|
const SESSION = {
|
|
@@ -211,7 +211,7 @@ async function clickSave(): Promise<void> {
|
|
|
211
211
|
// ── 1. The frame (AC-1, AC-4) ──────────────────────────────────────────────────
|
|
212
212
|
|
|
213
213
|
describe("AC-1 — the frame (D2, D7)", () => {
|
|
214
|
-
it("renders in the settings frame: Back to the Inbox, and the
|
|
214
|
+
it("renders in the settings frame: Back to the Inbox, and the five tabs with General current (#8438 D14 adds Privacy)", async () => {
|
|
215
215
|
await renderPage();
|
|
216
216
|
expect(screen.getByTestId("account-settings")).toContainElement(
|
|
217
217
|
screen.getByRole("heading", { level: 1, name: "General" }),
|
|
@@ -224,6 +224,7 @@ describe("AC-1 — the frame (D2, D7)", () => {
|
|
|
224
224
|
["General", "/account/general"],
|
|
225
225
|
["Security", "/account/security"],
|
|
226
226
|
["Notifications", "/account/notifications"],
|
|
227
|
+
["Privacy", "/account/privacy"],
|
|
227
228
|
["Billing", "/account/billing"],
|
|
228
229
|
]);
|
|
229
230
|
expect(links[0]).toHaveAttribute("aria-current", "page");
|
|
@@ -239,6 +240,7 @@ describe("AC-1 — the frame (D2, D7)", () => {
|
|
|
239
240
|
"href",
|
|
240
241
|
"/fr/account/notifications",
|
|
241
242
|
);
|
|
243
|
+
expect(screen.getByTestId("account-settings-rail-privacy")).toHaveAttribute("href", "/fr/account/privacy");
|
|
242
244
|
expect(screen.getByTestId("account-settings-rail-billing")).toHaveAttribute("href", "/fr/account/billing");
|
|
243
245
|
});
|
|
244
246
|
|
|
@@ -252,19 +254,19 @@ describe("AC-1 — the frame (D2, D7)", () => {
|
|
|
252
254
|
}
|
|
253
255
|
});
|
|
254
256
|
|
|
255
|
-
it("the pane holds the email read-only, the name and photo inputs
|
|
257
|
+
it("the pane holds the email read-only, the name and photo inputs", async () => {
|
|
256
258
|
await renderPage();
|
|
257
259
|
const pane = screen.getByTestId("account-settings-general");
|
|
258
260
|
expect(within(pane).getByTestId("account-general-email")).toHaveTextContent("ada@example.com");
|
|
259
261
|
expect(within(pane).queryByRole("textbox", { name: "Email" })).not.toBeInTheDocument();
|
|
260
262
|
expect(within(pane).getByLabelText("Name")).toHaveValue("Ada Lovelace");
|
|
261
263
|
expect(within(pane).getByLabelText("Photo")).toHaveValue(PHOTO);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("renders no deletion block — Request account deletion moved to Privacy (#8438 D16)", async () => {
|
|
267
|
+
await renderPage();
|
|
268
|
+
expect(screen.queryByTestId("account-deletion")).not.toBeInTheDocument();
|
|
269
|
+
expect(screen.queryByTestId("account-deletion-request")).not.toBeInTheDocument();
|
|
268
270
|
});
|
|
269
271
|
|
|
270
272
|
it("the photo is never rendered as an image", async () => {
|
|
@@ -284,7 +286,7 @@ describe("AC-1 — the frame (D2, D7)", () => {
|
|
|
284
286
|
expect(within(control).getAllByRole("radio").map((r) => r.textContent)).toEqual(["Light", "Auto", "Dark"]);
|
|
285
287
|
});
|
|
286
288
|
|
|
287
|
-
it("a contributed settings section renders after the
|
|
289
|
+
it("a contributed settings section renders after the general form (C8)", async () => {
|
|
288
290
|
const section: AccountSettingsSection = {
|
|
289
291
|
id: "extra",
|
|
290
292
|
title: "settings.general.title",
|
|
@@ -293,9 +295,9 @@ describe("AC-1 — the frame (D2, D7)", () => {
|
|
|
293
295
|
accountSettingsSections.push(section);
|
|
294
296
|
try {
|
|
295
297
|
await renderPage();
|
|
296
|
-
const
|
|
298
|
+
const form = screen.getByTestId("account-general-form");
|
|
297
299
|
const extra = screen.getByTestId("account-settings-extra");
|
|
298
|
-
expect(
|
|
300
|
+
expect(form.compareDocumentPosition(extra) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
299
301
|
expect(screen.getByTestId("extra-body")).toBeInTheDocument();
|
|
300
302
|
} finally {
|
|
301
303
|
accountSettingsSections.splice(accountSettingsSections.indexOf(section), 1);
|
|
@@ -387,7 +389,6 @@ describe("AC-3 — name and photo through the member API (D4, D8)", () => {
|
|
|
387
389
|
mockPieces.mockResolvedValue(refusal(503, "tenant_unresolved"));
|
|
388
390
|
await renderPage();
|
|
389
391
|
expect(screen.getByLabelText("Photo")).toBeInTheDocument();
|
|
390
|
-
expect(screen.getByTestId("account-deletion-request")).toBeInTheDocument();
|
|
391
392
|
});
|
|
392
393
|
});
|
|
393
394
|
|
|
@@ -494,13 +495,6 @@ describe("AC-3 — a single site with no member API (a 404 from the pieces read)
|
|
|
494
495
|
);
|
|
495
496
|
expect(screen.getByLabelText("Name")).toHaveValue("Ada King");
|
|
496
497
|
});
|
|
497
|
-
|
|
498
|
-
it("points the deletion request at Support, with no Request button (C2, D5)", async () => {
|
|
499
|
-
await renderPage();
|
|
500
|
-
expect(screen.queryByTestId("account-deletion-request")).not.toBeInTheDocument();
|
|
501
|
-
expect(screen.getByTestId("account-deletion-support")).toHaveAttribute("href", "/account/support");
|
|
502
|
-
expect(screen.getByText("To ask for your account to be deleted, contact support.")).toBeInTheDocument();
|
|
503
|
-
});
|
|
504
498
|
});
|
|
505
499
|
|
|
506
500
|
// ── #8435 D13 (AC-6) — the contact details on this site ───────────────────────
|
|
@@ -527,23 +521,23 @@ describe("AC-6 — the contact details (#8435 D13)", () => {
|
|
|
527
521
|
await renderPage();
|
|
528
522
|
const section = screen.getByTestId("account-contact-section");
|
|
529
523
|
expect(mockContact).toHaveBeenCalledWith(COOKIE_INIT);
|
|
530
|
-
expect(within(section).getByTestId("
|
|
531
|
-
expect(within(section).getByTestId("
|
|
524
|
+
expect(within(section).getByTestId("contact-given-name")).toHaveValue("Ada");
|
|
525
|
+
expect(within(section).getByTestId("contact-company")).toHaveValue("Engines Ltd");
|
|
532
526
|
expect(within(section).getByTestId("field-addressLine1")).toHaveValue("1 Analytical Way");
|
|
533
527
|
// The profile form's single "Save changes" button stays unique.
|
|
534
528
|
expect(screen.getAllByRole("button", { name: "Save changes" })).toHaveLength(1);
|
|
535
|
-
expect(within(section).getByTestId("
|
|
529
|
+
expect(within(section).getByTestId("contact-details-save")).toHaveTextContent("Save contact details");
|
|
536
530
|
});
|
|
537
531
|
|
|
538
532
|
it("with no confirmed contact yet, the form starts blank", async () => {
|
|
539
533
|
await renderPage();
|
|
540
|
-
expect(screen.getByTestId("
|
|
534
|
+
expect(screen.getByTestId("contact-given-name")).toHaveValue("");
|
|
541
535
|
});
|
|
542
536
|
|
|
543
537
|
it("a failed read renders the section's unavailable line, not the form", async () => {
|
|
544
538
|
mockContact.mockResolvedValue(refusal(503, "tenant_unresolved"));
|
|
545
539
|
await renderPage();
|
|
546
|
-
expect(screen.queryByTestId("
|
|
540
|
+
expect(screen.queryByTestId("contact-details-form")).not.toBeInTheDocument();
|
|
547
541
|
expect(screen.getByTestId("account-contact-unavailable")).toHaveTextContent(
|
|
548
542
|
"Contact details aren't available on this site right now.",
|
|
549
543
|
);
|
|
@@ -565,9 +559,9 @@ describe("AC-6 — the contact details (#8435 D13)", () => {
|
|
|
565
559
|
it("saving posts all six keys through saveContact and answers Saved", async () => {
|
|
566
560
|
mockContact.mockResolvedValue(ok({ contact: CONTACT }));
|
|
567
561
|
await renderPage();
|
|
568
|
-
fireEvent.change(screen.getByTestId("
|
|
562
|
+
fireEvent.change(screen.getByTestId("contact-company"), { target: { value: "Acme" } });
|
|
569
563
|
await act(async () => {
|
|
570
|
-
fireEvent.submit(screen.getByTestId("
|
|
564
|
+
fireEvent.submit(screen.getByTestId("contact-details-form"));
|
|
571
565
|
});
|
|
572
566
|
await waitFor(() => expect(mockUpdateContact).toHaveBeenCalledTimes(1));
|
|
573
567
|
expect(mockUpdateContact).toHaveBeenCalledWith(
|
|
@@ -581,20 +575,20 @@ describe("AC-6 — the contact details (#8435 D13)", () => {
|
|
|
581
575
|
},
|
|
582
576
|
COOKIE_INIT,
|
|
583
577
|
);
|
|
584
|
-
expect(await screen.findByTestId("
|
|
578
|
+
expect(await screen.findByTestId("contact-details-message")).toHaveTextContent("Saved.");
|
|
585
579
|
});
|
|
586
580
|
|
|
587
581
|
it("a refused telephone shows its own line and keeps the fields as typed", async () => {
|
|
588
582
|
mockUpdateContact.mockResolvedValue(refusal(400, "telephone_invalid"));
|
|
589
583
|
await renderPage();
|
|
590
|
-
fireEvent.change(screen.getByTestId("
|
|
584
|
+
fireEvent.change(screen.getByTestId("contact-company"), { target: { value: "Acme" } });
|
|
591
585
|
await act(async () => {
|
|
592
|
-
fireEvent.submit(screen.getByTestId("
|
|
586
|
+
fireEvent.submit(screen.getByTestId("contact-details-form"));
|
|
593
587
|
});
|
|
594
|
-
expect(await screen.findByTestId("
|
|
588
|
+
expect(await screen.findByTestId("contact-details-message")).toHaveTextContent(
|
|
595
589
|
"Enter the phone number with its country code.",
|
|
596
590
|
);
|
|
597
|
-
expect(screen.getByTestId("
|
|
591
|
+
expect(screen.getByTestId("contact-company")).toHaveValue("Acme");
|
|
598
592
|
});
|
|
599
593
|
});
|
|
600
594
|
|
|
@@ -638,57 +632,8 @@ describe("AC-6 — saveContact (#8435 D13)", () => {
|
|
|
638
632
|
});
|
|
639
633
|
});
|
|
640
634
|
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
describe("AC-4 — Request account deletion through the member API (D5)", () => {
|
|
644
|
-
async function requestDeletion(): Promise<void> {
|
|
645
|
-
await act(async () => {
|
|
646
|
-
fireEvent.click(screen.getByTestId("account-deletion-request"));
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
it.each([
|
|
651
|
-
["site-operator", "Sent to the site operator."],
|
|
652
|
-
["hub-operator", "Sent to the hub operator."],
|
|
653
|
-
])("an answer naming %s shows %s — the answer picks the line", async (sentTo, line) => {
|
|
654
|
-
mockRequestDeletion.mockResolvedValue(ok({ sentTo }));
|
|
655
|
-
await renderPage();
|
|
656
|
-
await requestDeletion();
|
|
657
|
-
expect(mockRequestDeletion).toHaveBeenCalledWith({}, COOKIE_INIT);
|
|
658
|
-
await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent(line));
|
|
659
|
-
expect(screen.queryByTestId("account-deletion-request")).not.toBeInTheDocument();
|
|
660
|
-
expect(mockLeadsSubmit).not.toHaveBeenCalled();
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
it("a refusal shows its line and keeps the button", async () => {
|
|
664
|
-
mockRequestDeletion.mockResolvedValue(refusal(403, "email_unverified"));
|
|
665
|
-
await renderPage();
|
|
666
|
-
await requestDeletion();
|
|
667
|
-
await waitFor(() =>
|
|
668
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Confirm your email address first, then ask again."),
|
|
669
|
-
);
|
|
670
|
-
expect(screen.getByTestId("account-deletion-request")).toBeEnabled();
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
it.each([
|
|
674
|
-
[refusal(403, "email_unverified"), "unverified"],
|
|
675
|
-
[refusal(404, "not_found"), "unavailable"],
|
|
676
|
-
[refusal(503, "not_configured"), "unavailable"],
|
|
677
|
-
[refusal(503, "delivery_failed"), "unavailable"],
|
|
678
|
-
[refusal(500, "internal"), "failed"],
|
|
679
|
-
])("requestAccountDeletion maps %j to %s", async (answer, reason) => {
|
|
680
|
-
mockRequestDeletion.mockResolvedValue(answer);
|
|
681
|
-
await expect(requestAccountDeletion()).resolves.toEqual({ ok: false, reason });
|
|
682
|
-
expect(mockRequestDeletion).toHaveBeenCalledWith({}, COOKIE_INIT);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it("requestAccountDeletion answers who it went to, and a throw is failed", async () => {
|
|
686
|
-
mockRequestDeletion.mockResolvedValue(ok({ sentTo: "hub-operator" }));
|
|
687
|
-
await expect(requestAccountDeletion()).resolves.toEqual({ ok: true, sentTo: "hub-operator" });
|
|
688
|
-
mockRequestDeletion.mockRejectedValue(new TypeError("fetch failed"));
|
|
689
|
-
await expect(requestAccountDeletion()).resolves.toEqual({ ok: false, reason: "failed" });
|
|
690
|
-
});
|
|
691
|
-
});
|
|
635
|
+
// The deletion-request cases moved to privacy.test.tsx — Request account
|
|
636
|
+
// deletion now renders on Privacy, not General (#8438 D16).
|
|
692
637
|
|
|
693
638
|
// ── 5. #billing (AC-6) ────────────────────────────────────────────────────────
|
|
694
639
|
|
|
@@ -248,13 +248,12 @@ describe("AC-5 — General and Billing on a hub-mode spoke (#8232)", () => {
|
|
|
248
248
|
mockMemberRead.mockResolvedValue({ ok: true, body: [] });
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it("renders the name and photo form
|
|
251
|
+
it("renders the name and photo form, with no link to the hub's settings", async () => {
|
|
252
252
|
await renderGeneral();
|
|
253
253
|
expect(screen.getByTestId("account-general-form")).toBeInTheDocument();
|
|
254
254
|
expect(screen.getByLabelText("Name")).toHaveValue("Ada Lovelace");
|
|
255
255
|
// No hub read carries the photo, so the field starts empty (C5).
|
|
256
256
|
expect(screen.getByLabelText("Photo")).toHaveValue("");
|
|
257
|
-
expect(screen.getByTestId("account-deletion-request")).toBeInTheDocument();
|
|
258
257
|
expect(screen.queryByTestId("account-settings-manage-on-hub")).not.toBeInTheDocument();
|
|
259
258
|
expect(screen.queryByTestId("account-privacy-manage-on-hub")).not.toBeInTheDocument();
|
|
260
259
|
expect(document.querySelector(`a[href^="${HUB}"]`)).toBeNull();
|
|
@@ -279,10 +278,32 @@ describe("AC-5 — General and Billing on a hub-mode spoke (#8232)", () => {
|
|
|
279
278
|
user: { name: "Ada King" },
|
|
280
279
|
});
|
|
281
280
|
});
|
|
281
|
+
});
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
describe("a hub that answers no member route (the fixture's 404)", () => {
|
|
284
|
+
it("shows the name read-only and offers no save", async () => {
|
|
285
285
|
await renderGeneral();
|
|
286
|
+
expect(screen.getByTestId("account-general-name-text")).toHaveTextContent("Ada Lovelace");
|
|
287
|
+
expect(screen.queryByTestId("account-general-form")).not.toBeInTheDocument();
|
|
288
|
+
expect(screen.queryByLabelText("Photo")).not.toBeInTheDocument();
|
|
289
|
+
expect(document.querySelector(`a[href^="${HUB}"]`)).toBeNull();
|
|
290
|
+
expect(mockUpdateUser).not.toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// #8438 D16 — Request account deletion moved to Privacy; the hub-mode
|
|
295
|
+
// deletion coverage above moves with it, expectations unchanged.
|
|
296
|
+
describe("Privacy's deletion block on a hub-mode spoke (moved from General, #8438 D16)", () => {
|
|
297
|
+
async function renderPrivacy(): Promise<void> {
|
|
298
|
+
const { default: PrivacyPage } = await import("../privacy/page");
|
|
299
|
+
render(await PrivacyPage({ params: Promise.resolve({ locale: "en" }) }));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
it("a hub that answers the member routes: the deletion request goes through the member API and names the hub operator", async () => {
|
|
303
|
+
mockMemberRead.mockResolvedValue({ ok: true, body: [] });
|
|
304
|
+
mockRequestDeletion.mockResolvedValue({ ok: true, body: { sentTo: "hub-operator" } });
|
|
305
|
+
await renderPrivacy();
|
|
306
|
+
expect(screen.getByTestId("account-deletion-request")).toBeInTheDocument();
|
|
286
307
|
await act(async () => {
|
|
287
308
|
fireEvent.click(screen.getByTestId("account-deletion-request"));
|
|
288
309
|
});
|
|
@@ -290,19 +311,12 @@ describe("AC-5 — General and Billing on a hub-mode spoke (#8232)", () => {
|
|
|
290
311
|
await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent("Sent to the hub operator."));
|
|
291
312
|
expect(mockLeadsSubmit).not.toHaveBeenCalled();
|
|
292
313
|
});
|
|
293
|
-
});
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
await renderGeneral();
|
|
298
|
-
expect(screen.getByTestId("account-general-name-text")).toHaveTextContent("Ada Lovelace");
|
|
299
|
-
expect(screen.queryByTestId("account-general-form")).not.toBeInTheDocument();
|
|
300
|
-
expect(screen.queryByLabelText("Photo")).not.toBeInTheDocument();
|
|
315
|
+
it("a hub that answers no member route: offers no deletion path", async () => {
|
|
316
|
+
await renderPrivacy();
|
|
301
317
|
expect(screen.queryByTestId("account-deletion-request")).not.toBeInTheDocument();
|
|
302
318
|
expect(screen.queryByTestId("account-deletion-support")).not.toBeInTheDocument();
|
|
303
319
|
expect(screen.getByText("This site can't take deletion requests right now.")).toBeInTheDocument();
|
|
304
|
-
expect(document.querySelector(`a[href^="${HUB}"]`)).toBeNull();
|
|
305
|
-
expect(mockUpdateUser).not.toHaveBeenCalled();
|
|
306
320
|
});
|
|
307
321
|
});
|
|
308
322
|
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* 2. An unknown kind, or an id the kind did not load, is a not-found —
|
|
10
10
|
* never an empty frame.
|
|
11
11
|
* 3. A failed read renders an error with Retry (D12's posture).
|
|
12
|
+
* 4. A message whose `action` is `onboarding` has an Open onboarding
|
|
13
|
+
* action to the locale's `/account/onboarding`; a message with no
|
|
14
|
+
* action has none (#8515 D6, AC-4).
|
|
12
15
|
*
|
|
13
|
-
* The `purchase` kind reads `member.purchases` (#8231 D5)
|
|
16
|
+
* The `purchase` kind reads `member.purchases` (#8231 D5); the `message`
|
|
17
|
+
* kind reads `member.messages`.
|
|
14
18
|
*/
|
|
15
19
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
16
20
|
|
|
@@ -52,11 +56,15 @@ jest.mock("../../../../../auth", () => ({
|
|
|
52
56
|
|
|
53
57
|
const mockSignupMe = jest.fn();
|
|
54
58
|
const mockMemberPurchases = jest.fn();
|
|
59
|
+
const mockMemberMessages = jest.fn();
|
|
55
60
|
jest.mock("@working-theory/api-client", () => ({
|
|
56
61
|
__esModule: true,
|
|
57
62
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
58
63
|
subscription: { me: jest.fn() },
|
|
59
|
-
member: {
|
|
64
|
+
member: {
|
|
65
|
+
purchases: (...args: unknown[]) => mockMemberPurchases(...args),
|
|
66
|
+
messages: (...args: unknown[]) => mockMemberMessages(...args),
|
|
67
|
+
},
|
|
60
68
|
}));
|
|
61
69
|
|
|
62
70
|
const mockNotFound = jest.fn(() => {
|
|
@@ -85,9 +93,30 @@ const PURCHASE = {
|
|
|
85
93
|
purchasedAt: "2026-09-04T10:00:00.000Z",
|
|
86
94
|
};
|
|
87
95
|
|
|
96
|
+
const MESSAGE = {
|
|
97
|
+
id: "int_plain",
|
|
98
|
+
subject: "Hello",
|
|
99
|
+
body: "A note.",
|
|
100
|
+
file: null,
|
|
101
|
+
action: null,
|
|
102
|
+
createdAt: "2026-10-14T09:00:00.000Z",
|
|
103
|
+
readAt: null,
|
|
104
|
+
};
|
|
105
|
+
const MESSAGES = [
|
|
106
|
+
MESSAGE,
|
|
107
|
+
{
|
|
108
|
+
...MESSAGE,
|
|
109
|
+
id: "int_onboarding",
|
|
110
|
+
subject: "Your onboarding questions",
|
|
111
|
+
body: "Open this message in your account and choose Open onboarding.",
|
|
112
|
+
action: "onboarding",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
88
116
|
beforeEach(() => {
|
|
89
117
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
90
118
|
mockMemberPurchases.mockReset().mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
|
|
119
|
+
mockMemberMessages.mockReset().mockResolvedValue({ ok: true, body: { items: MESSAGES } });
|
|
91
120
|
mockSignupMe.mockReset();
|
|
92
121
|
mockNotFound.mockClear();
|
|
93
122
|
mockRefresh.mockReset();
|
|
@@ -139,3 +168,29 @@ describe("one Inbox item", () => {
|
|
|
139
168
|
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
140
169
|
});
|
|
141
170
|
});
|
|
171
|
+
|
|
172
|
+
describe("a message's onboarding action (#8515 D6, AC-4)", () => {
|
|
173
|
+
it("an onboarding message links to the locale's onboarding page", async () => {
|
|
174
|
+
await renderItem("message", "int_onboarding", "fr");
|
|
175
|
+
const links = screen
|
|
176
|
+
.getAllByRole("link", { name: "Open onboarding" })
|
|
177
|
+
.map((link) => link.getAttribute("href"));
|
|
178
|
+
expect(links.length).toBeGreaterThan(0);
|
|
179
|
+
expect(links.every((href) => href === "/fr/account/onboarding")).toBe(true);
|
|
180
|
+
expect(screen.getByTestId("account-item-action-onboarding-desk")).toHaveAttribute(
|
|
181
|
+
"href",
|
|
182
|
+
"/fr/account/onboarding",
|
|
183
|
+
);
|
|
184
|
+
// The body stays plain text.
|
|
185
|
+
expect(
|
|
186
|
+
screen.getByText("Open this message in your account and choose Open onboarding.").closest("a"),
|
|
187
|
+
).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("a message with no action has no onboarding link", async () => {
|
|
191
|
+
await renderItem("message", "int_plain", "fr");
|
|
192
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Hello");
|
|
193
|
+
expect(screen.queryByRole("link", { name: "Open onboarding" })).not.toBeInTheDocument();
|
|
194
|
+
expect(screen.queryByTestId("account-item-action-onboarding-desk")).not.toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -472,6 +472,7 @@ describe("the settings routes take the screen (#8430)", () => {
|
|
|
472
472
|
["/account/general", "en"],
|
|
473
473
|
["/account/security", "en"],
|
|
474
474
|
["/account/notifications", "en"],
|
|
475
|
+
["/account/privacy", "en"],
|
|
475
476
|
["/account/billing", "en"],
|
|
476
477
|
["/fr/account/billing", "fr"],
|
|
477
478
|
])("on %s the page renders alone — no rail, no Inbox, no AppShell frame", async (path, locale) => {
|
|
@@ -527,6 +528,7 @@ describe("one main landmark (#8441)", () => {
|
|
|
527
528
|
general: async () => (await import("../general/page")).default,
|
|
528
529
|
security: async () => (await import("../security/page")).default,
|
|
529
530
|
notifications: async () => (await import("../notifications/page")).default,
|
|
531
|
+
privacy: async () => (await import("../privacy/page")).default,
|
|
530
532
|
billing: async () => (await import("../billing/page")).default,
|
|
531
533
|
};
|
|
532
534
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for /[locale]/account/notifications — User settings › Notifications
|
|
3
|
-
* (#8339 D8,
|
|
4
|
-
*
|
|
3
|
+
* (#8339 D8; #8438 D15, AC-8). Always renders (decision A): one
|
|
4
|
+
* `ConsentSwitchList` of the registry's `area: "communications"` purposes,
|
|
5
|
+
* or one empty line with none declared. The cookie-settings link moved to
|
|
6
|
+
* Privacy (#8438 D14) — this tab no longer renders it.
|
|
5
7
|
*/
|
|
6
8
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
9
|
import { tmpdir } from "node:os";
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
|
|
10
|
-
import { render, screen, within } from "@testing-library/react";
|
|
12
|
+
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
11
13
|
|
|
12
14
|
jest.mock("next-intl/server", () => {
|
|
13
15
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
@@ -29,13 +31,14 @@ jest.mock("next-intl/server", () => {
|
|
|
29
31
|
);
|
|
30
32
|
};
|
|
31
33
|
},
|
|
34
|
+
getLocale: jest.fn().mockResolvedValue("en"),
|
|
32
35
|
};
|
|
33
36
|
});
|
|
34
37
|
|
|
35
38
|
jest.mock("next/headers", () => ({
|
|
36
39
|
__esModule: true,
|
|
37
40
|
headers: jest.fn().mockResolvedValue(new Headers()),
|
|
38
|
-
cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
|
|
41
|
+
cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc", get: () => undefined }),
|
|
39
42
|
}));
|
|
40
43
|
|
|
41
44
|
const mockGetSession = jest.fn();
|
|
@@ -45,13 +48,19 @@ jest.mock("../../../../../auth", () => ({
|
|
|
45
48
|
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
46
49
|
}));
|
|
47
50
|
|
|
51
|
+
const mockConsents = jest.fn();
|
|
52
|
+
const mockSetConsent = jest.fn();
|
|
48
53
|
jest.mock("@working-theory/api-client", () => ({
|
|
49
54
|
__esModule: true,
|
|
50
55
|
waitlistSignups: { me: jest.fn() },
|
|
51
56
|
flowCheckouts: { me: jest.fn() },
|
|
52
57
|
subscription: { me: jest.fn(), portalSession: jest.fn() },
|
|
53
58
|
leads: { submit: jest.fn() },
|
|
54
|
-
member: {
|
|
59
|
+
member: {
|
|
60
|
+
pieces: jest.fn().mockResolvedValue({ ok: true, body: [] }),
|
|
61
|
+
consents: (...args: unknown[]) => mockConsents(...args),
|
|
62
|
+
setConsent: (...args: unknown[]) => mockSetConsent(...args),
|
|
63
|
+
},
|
|
55
64
|
hubSignIn: { me: jest.fn() },
|
|
56
65
|
visitorForwardHeaders: () => null,
|
|
57
66
|
}));
|
|
@@ -75,6 +84,31 @@ const SESSION = {
|
|
|
75
84
|
},
|
|
76
85
|
};
|
|
77
86
|
|
|
87
|
+
const ok = <T,>(body: T) => ({ ok: true, body });
|
|
88
|
+
|
|
89
|
+
function communicationsPurpose(
|
|
90
|
+
overrides: Partial<{
|
|
91
|
+
id: string;
|
|
92
|
+
kind: "consent" | "preference";
|
|
93
|
+
effective: boolean;
|
|
94
|
+
state: { granted: boolean; at: string; source: string; policyVersion: number } | null;
|
|
95
|
+
}> = {},
|
|
96
|
+
) {
|
|
97
|
+
return {
|
|
98
|
+
id: overrides.id ?? "marketing_email",
|
|
99
|
+
kind: overrides.kind ?? "consent",
|
|
100
|
+
area: "communications" as const,
|
|
101
|
+
required: false,
|
|
102
|
+
signUp: false,
|
|
103
|
+
description: "Product news and offers.",
|
|
104
|
+
statement: "Receive product news and offers by email.",
|
|
105
|
+
policyHref: "/privacy",
|
|
106
|
+
policyVersion: 1,
|
|
107
|
+
state: overrides.state === undefined ? null : overrides.state,
|
|
108
|
+
effective: overrides.effective ?? false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
let contentRoot: string;
|
|
79
113
|
|
|
80
114
|
beforeEach(() => {
|
|
@@ -82,6 +116,8 @@ beforeEach(() => {
|
|
|
82
116
|
process.env.AILK_CONTENT_ROOT = contentRoot;
|
|
83
117
|
writeFileSync(join(contentRoot, "_site.mdx"), '---\nsiteName: "Test"\n---\n');
|
|
84
118
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
119
|
+
mockConsents.mockReset().mockResolvedValue(ok({ purposes: [] }));
|
|
120
|
+
mockSetConsent.mockReset();
|
|
85
121
|
});
|
|
86
122
|
|
|
87
123
|
afterEach(() => {
|
|
@@ -98,7 +134,7 @@ describe("the frame (D7, D8)", () => {
|
|
|
98
134
|
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Notifications");
|
|
99
135
|
expect(screen.getByTestId("account-settings-back")).toHaveAttribute("href", "/account");
|
|
100
136
|
expect(screen.getByTestId("account-settings-rail-notifications")).toHaveAttribute("aria-current", "page");
|
|
101
|
-
expect(screen.getByTestId("account-settings-rail-
|
|
137
|
+
expect(screen.getByTestId("account-settings-rail-privacy")).not.toHaveAttribute("aria-current");
|
|
102
138
|
});
|
|
103
139
|
});
|
|
104
140
|
|
|
@@ -116,25 +152,73 @@ describe("the standalone shape (#8430)", () => {
|
|
|
116
152
|
});
|
|
117
153
|
});
|
|
118
154
|
|
|
119
|
-
describe("
|
|
120
|
-
it("
|
|
155
|
+
describe("AC-8 — always present, one empty line with no communications purposes declared", () => {
|
|
156
|
+
it("renders exactly one account-notifications-empty line and no cookie link", async () => {
|
|
157
|
+
await renderPage();
|
|
158
|
+
expect(screen.getByTestId("account-notifications-empty")).toHaveTextContent(
|
|
159
|
+
"There's nothing to set here yet.",
|
|
160
|
+
);
|
|
161
|
+
expect(screen.queryByTestId("account-cookie-settings")).not.toBeInTheDocument();
|
|
162
|
+
expect(screen.queryByTestId("account-notifications")).not.toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("the tab link is present in the frame regardless", async () => {
|
|
166
|
+
await renderPage();
|
|
167
|
+
expect(screen.getByTestId("account-settings-rail-notifications")).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("AC-8 — a declared communications preference defaults on with no row", () => {
|
|
172
|
+
it("renders one switch, on", async () => {
|
|
173
|
+
mockConsents.mockResolvedValue(
|
|
174
|
+
ok({ purposes: [communicationsPurpose({ kind: "preference", effective: true, state: null })] }),
|
|
175
|
+
);
|
|
121
176
|
await renderPage();
|
|
122
|
-
expect(screen.
|
|
123
|
-
|
|
124
|
-
|
|
177
|
+
expect(screen.queryByTestId("account-notifications-empty")).not.toBeInTheDocument();
|
|
178
|
+
expect(screen.getByTestId("account-notifications-marketing_email-switch")).toBeChecked();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("AC-8 — a declared communications consent defaults off with no row", () => {
|
|
183
|
+
it("renders one switch, off", async () => {
|
|
184
|
+
mockConsents.mockResolvedValue(
|
|
185
|
+
ok({ purposes: [communicationsPurpose({ kind: "consent", effective: false, state: null })] }),
|
|
125
186
|
);
|
|
187
|
+
await renderPage();
|
|
188
|
+
expect(screen.getByTestId("account-notifications-marketing_email-switch")).not.toBeChecked();
|
|
126
189
|
});
|
|
127
190
|
|
|
128
|
-
it("
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
191
|
+
it("toggling calls setConsentAction through member.setConsent", async () => {
|
|
192
|
+
mockConsents.mockResolvedValue(
|
|
193
|
+
ok({ purposes: [communicationsPurpose({ kind: "consent", effective: false, state: null })] }),
|
|
194
|
+
);
|
|
195
|
+
mockSetConsent.mockResolvedValue(
|
|
196
|
+
ok({
|
|
197
|
+
purpose: communicationsPurpose({
|
|
198
|
+
kind: "consent",
|
|
199
|
+
effective: true,
|
|
200
|
+
state: { granted: true, at: "2026-10-12T12:00:00.000Z", source: "account_settings", policyVersion: 1 },
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
await renderPage();
|
|
205
|
+
fireEvent.click(screen.getByTestId("account-notifications-marketing_email-switch"));
|
|
206
|
+
await waitFor(() => expect(mockSetConsent).toHaveBeenCalledTimes(1));
|
|
207
|
+
expect(mockSetConsent).toHaveBeenCalledWith(
|
|
208
|
+
{ purposeId: "marketing_email", granted: true, locale: "en" },
|
|
209
|
+
expect.objectContaining({ headers: expect.objectContaining({ cookie: "session=abc" }) }),
|
|
210
|
+
);
|
|
211
|
+
expect(await screen.findByTestId("account-notifications-marketing_email-date")).toHaveTextContent(
|
|
212
|
+
"Given Oct 12, 2026",
|
|
133
213
|
);
|
|
134
214
|
});
|
|
215
|
+
});
|
|
135
216
|
|
|
136
|
-
|
|
217
|
+
describe("a failed consents read", () => {
|
|
218
|
+
it("shows the unavailable line, not a broken switch list", async () => {
|
|
219
|
+
mockConsents.mockResolvedValue({ ok: false, status: 503, body: null });
|
|
137
220
|
await renderPage();
|
|
138
|
-
expect(screen.
|
|
221
|
+
expect(screen.getByTestId("account-notifications-unavailable")).toBeInTheDocument();
|
|
222
|
+
expect(screen.queryByTestId("account-notifications-empty")).not.toBeInTheDocument();
|
|
139
223
|
});
|
|
140
224
|
});
|