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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for /[locale]/account — the base account Inbox (#7430, spec 7423
|
|
3
|
-
* U4 · D6a, D9, D12; AC-7 as amended 2026-10-01).
|
|
3
|
+
* U4 · D6a, D9, D12; AC-7 as amended 2026-10-01; #8231 D3, D5–D7).
|
|
4
4
|
*
|
|
5
5
|
* Covers:
|
|
6
|
-
* 1. The
|
|
7
|
-
*
|
|
8
|
-
* 2.
|
|
9
|
-
*
|
|
6
|
+
* 1. The tabs All · Purchases · Scheduled, each with its count (zeros
|
|
7
|
+
* included), All selected (#8231 D3).
|
|
8
|
+
* 2. All holds every row newest first, each linking to its item page;
|
|
9
|
+
* Purchases holds the kinds whose section is `purchases`; Scheduled the
|
|
10
|
+
* `upcoming` kinds, upcoming soonest first, then past newest first.
|
|
10
11
|
* 3. `email-unverified` and `payment-due` are For-you items (the phone
|
|
11
12
|
* For you tab), never Inbox rows.
|
|
12
13
|
* 4. A member with no name gets an "Add your name" For-you item (needs-you,
|
|
@@ -20,13 +21,13 @@
|
|
|
20
21
|
* 7. #7920 — the member area's `piece` and `message` kinds and the
|
|
21
22
|
* needs-client For-you source. Each reads the hub with `ctx.init`
|
|
22
23
|
* unchanged (the caller's own credential); 404 and an unverified 401/403
|
|
23
|
-
* load `[]`, any other failure rejects (D12).
|
|
24
|
-
*
|
|
24
|
+
* load `[]`, any other failure rejects (D12). On a cookie-session site
|
|
25
|
+
* they call with the cookie too — no gate (#8231 D7).
|
|
25
26
|
*/
|
|
26
27
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
27
28
|
|
|
28
29
|
// The global setup mock resolves only top-level namespace keys against
|
|
29
|
-
// en.json; the area reads nested keys (`inbox.tabs.
|
|
30
|
+
// en.json; the area reads nested keys (`inbox.tabs.all`, …) — a call
|
|
30
31
|
// shape real next-intl supports — so resolve dotted namespaces AND dotted
|
|
31
32
|
// keys here against the same catalog. `{name}` placeholders are filled from
|
|
32
33
|
// `values` the way next-intl does.
|
|
@@ -67,18 +68,26 @@ jest.mock("../../../../../auth", () => ({
|
|
|
67
68
|
}));
|
|
68
69
|
|
|
69
70
|
const mockSignupMe = jest.fn();
|
|
70
|
-
const mockCheckoutsMe = jest.fn();
|
|
71
71
|
const mockSubscriptionMe = jest.fn();
|
|
72
72
|
const mockMemberPieces = jest.fn();
|
|
73
73
|
const mockMemberMessages = jest.fn();
|
|
74
|
+
const mockMemberPurchases = jest.fn();
|
|
75
|
+
const mockMemberAppointments = jest.fn();
|
|
74
76
|
jest.mock("@working-theory/api-client", () => ({
|
|
75
77
|
__esModule: true,
|
|
76
78
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
77
|
-
flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
|
|
78
79
|
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
79
80
|
member: {
|
|
80
81
|
pieces: (...args: unknown[]) => mockMemberPieces(...args),
|
|
81
82
|
messages: (...args: unknown[]) => mockMemberMessages(...args),
|
|
83
|
+
purchases: (...args: unknown[]) => mockMemberPurchases(...args),
|
|
84
|
+
appointments: (...args: unknown[]) => mockMemberAppointments(...args),
|
|
85
|
+
// #8233 C4 — the New menu's availability reads; 404 here, so the Inbox
|
|
86
|
+
// cases see no New action and no error row.
|
|
87
|
+
meetingTypes: () =>
|
|
88
|
+
Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
|
|
89
|
+
requestForm: () =>
|
|
90
|
+
Promise.resolve({ ok: false, status: 404, body: { ok: false, error: { code: "not_found", message: "Not found" } } }),
|
|
82
91
|
},
|
|
83
92
|
}));
|
|
84
93
|
|
|
@@ -123,19 +132,21 @@ const SIGNUP = {
|
|
|
123
132
|
answers: { q3: null },
|
|
124
133
|
};
|
|
125
134
|
|
|
126
|
-
const
|
|
127
|
-
id: "
|
|
128
|
-
|
|
129
|
-
experimentKey: "ailk-founder-diagnostic",
|
|
130
|
-
offerKey: "pro",
|
|
131
|
-
status: "completed",
|
|
135
|
+
const PURCHASE = {
|
|
136
|
+
id: "pur_done",
|
|
137
|
+
offering: "pro",
|
|
132
138
|
amountMinor: 4900,
|
|
133
139
|
currency: "usd",
|
|
134
140
|
period: "monthly",
|
|
135
|
-
|
|
136
|
-
createdAt: "2026-09-04T09:58:00.000Z",
|
|
141
|
+
purchasedAt: "2026-09-04T10:00:00.000Z",
|
|
137
142
|
};
|
|
138
143
|
|
|
144
|
+
function appointment(id: string, title: string, startTime: string) {
|
|
145
|
+
return { id, title, startTime, endTime: startTime, status: "confirmed" as const };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const NO_APPOINTMENTS = { ok: true, body: { upcoming: [], past: [] } };
|
|
149
|
+
|
|
139
150
|
const NO_SUBSCRIPTION = {
|
|
140
151
|
ok: true,
|
|
141
152
|
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
@@ -236,74 +247,133 @@ beforeEach(() => {
|
|
|
236
247
|
accountExperimentKeys.length = 0;
|
|
237
248
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
238
249
|
mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
|
|
239
|
-
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [CHECKOUT] });
|
|
240
250
|
mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
mockMemberPurchases.mockReset().mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
|
|
252
|
+
mockMemberAppointments.mockReset().mockResolvedValue(NO_APPOINTMENTS);
|
|
253
|
+
// The page's pieces and messages answer 404 unless a case fills them.
|
|
254
|
+
mockMemberPieces.mockReset().mockResolvedValue(notFound);
|
|
255
|
+
mockMemberMessages.mockReset().mockResolvedValue(notFound);
|
|
243
256
|
mockRefresh.mockReset();
|
|
244
257
|
});
|
|
245
258
|
|
|
259
|
+
/** The member kinds' own cases read rows from both member reads. */
|
|
260
|
+
function memberRows(): void {
|
|
261
|
+
mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE, PIECE_NEEDS_CLIENT] } });
|
|
262
|
+
mockMemberMessages.mockResolvedValue({ ok: true, body: { items: [MESSAGE, MESSAGE_WITH_FILE] } });
|
|
263
|
+
}
|
|
264
|
+
|
|
246
265
|
afterAll(() => {
|
|
247
266
|
accountExperimentKeys.length = 0;
|
|
248
267
|
});
|
|
249
268
|
|
|
250
|
-
describe("the Inbox —
|
|
251
|
-
|
|
269
|
+
describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", () => {
|
|
270
|
+
/** The rows of one tab, by title, after selecting it. */
|
|
271
|
+
function tabRows(id: string): string[] {
|
|
272
|
+
fireEvent.mouseDown(screen.getByTestId(`inbox-tab-${id}`));
|
|
273
|
+
return within(screen.getByTestId(`inbox-${id}`))
|
|
274
|
+
.queryAllByTestId(`inbox-${id}-row`)
|
|
275
|
+
.map((row) => within(row).getByRole("link").textContent ?? "");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
it("renders All · Purchases · Scheduled with counts, All selected — no Recents or Suggested", async () => {
|
|
252
279
|
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
280
|
+
mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE] } });
|
|
281
|
+
mockMemberMessages.mockResolvedValue({ ok: true, body: { items: [MESSAGE] } });
|
|
282
|
+
mockMemberAppointments.mockResolvedValue({
|
|
283
|
+
ok: true,
|
|
284
|
+
body: { upcoming: [appointment("appt_1", "Kickoff call", "2026-09-10T15:00:00.000Z")], past: [] },
|
|
285
|
+
});
|
|
253
286
|
await renderPage();
|
|
254
287
|
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Inbox");
|
|
255
288
|
const tabs = screen.getAllByRole("tab");
|
|
256
289
|
expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
|
|
257
|
-
"inbox-tab-recents",
|
|
258
|
-
"inbox-tab-suggested",
|
|
259
|
-
"inbox-tab-scheduled",
|
|
260
290
|
"inbox-tab-all",
|
|
291
|
+
"inbox-tab-purchases",
|
|
292
|
+
"inbox-tab-scheduled",
|
|
261
293
|
]);
|
|
262
|
-
expect(tabs.map((tab) => tab.textContent)).toEqual([
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
294
|
+
expect(tabs.map((tab) => tab.textContent)).toEqual(["All5", "Purchases1", "Scheduled1"]);
|
|
295
|
+
expect(screen.getByTestId("inbox-tab-all")).toHaveAttribute("aria-selected", "true");
|
|
296
|
+
// All: every row, newest first.
|
|
297
|
+
const all = within(screen.getByTestId("inbox-all")).getAllByTestId("inbox-all-row");
|
|
298
|
+
expect(all.map((row) => row.textContent)).toEqual([
|
|
299
|
+
expect.stringContaining("Kickoff call"),
|
|
300
|
+
expect.stringContaining("Kickoff notes"),
|
|
301
|
+
expect.stringContaining("Spring newsletter"),
|
|
302
|
+
expect.stringContaining("pro"),
|
|
303
|
+
expect.stringContaining("ailk-founder-diagnostic"),
|
|
267
304
|
]);
|
|
268
|
-
|
|
305
|
+
// Purchases: the purchase row only. Scheduled: the appointment only.
|
|
306
|
+
expect(tabRows("purchases")).toEqual([expect.stringContaining("pro")]);
|
|
307
|
+
expect(tabRows("scheduled")).toEqual([expect.stringContaining("Kickoff call")]);
|
|
308
|
+
// The result row is under All and in no other tab.
|
|
309
|
+
expect(screen.queryByText("ailk-founder-diagnostic")).not.toBeInTheDocument();
|
|
269
310
|
});
|
|
270
311
|
|
|
271
|
-
it("
|
|
312
|
+
it("each All row links to its item page, prefixed off the default locale", async () => {
|
|
272
313
|
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
273
|
-
await renderPage();
|
|
274
|
-
const recents = screen.getByTestId("inbox-recents");
|
|
275
|
-
const rows = within(recents).getAllByTestId("inbox-recents-row");
|
|
276
|
-
expect(rows).toHaveLength(2);
|
|
277
|
-
// The purchase (Sep 4) is newer than the result (Sep 3).
|
|
278
|
-
expect(rows[0]).toHaveTextContent("pro");
|
|
279
|
-
expect(rows[1]).toHaveTextContent("ailk-founder-diagnostic");
|
|
280
|
-
expect(within(recents).getByText("pro").closest("a")).toHaveAttribute(
|
|
281
|
-
"href",
|
|
282
|
-
"/account/inbox/purchase/fc_done",
|
|
283
|
-
);
|
|
284
|
-
expect(
|
|
285
|
-
within(recents).getByText("ailk-founder-diagnostic").closest("a"),
|
|
286
|
-
).toHaveAttribute("href", "/account/inbox/result/ailk-founder-diagnostic");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("the same rows are in All; a non-default locale prefixes the item href", async () => {
|
|
290
314
|
await renderPage("fr");
|
|
291
|
-
fireEvent.mouseDown(screen.getByTestId("inbox-tab-all"));
|
|
292
315
|
const all = screen.getByTestId("inbox-all");
|
|
293
316
|
expect(within(all).getByText("pro").closest("a")).toHaveAttribute(
|
|
294
317
|
"href",
|
|
295
|
-
"/fr/account/inbox/purchase/
|
|
318
|
+
"/fr/account/inbox/purchase/pur_done",
|
|
296
319
|
);
|
|
320
|
+
expect(
|
|
321
|
+
within(all).getByText("ailk-founder-diagnostic").closest("a"),
|
|
322
|
+
).toHaveAttribute("href", "/fr/account/inbox/result/ailk-founder-diagnostic");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("Scheduled lists the upcoming soonest first, then the past newest first", async () => {
|
|
326
|
+
mockMemberAppointments.mockResolvedValue({
|
|
327
|
+
ok: true,
|
|
328
|
+
body: {
|
|
329
|
+
// Out of order on purpose: the page orders them, not the read.
|
|
330
|
+
upcoming: [
|
|
331
|
+
appointment("u1", "U1", "2026-10-25T15:00:00.000Z"),
|
|
332
|
+
appointment("u2", "U2", "2026-10-20T15:00:00.000Z"),
|
|
333
|
+
],
|
|
334
|
+
past: [
|
|
335
|
+
appointment("p1", "P1", "2026-09-01T15:00:00.000Z"),
|
|
336
|
+
appointment("p2", "P2", "2026-09-05T15:00:00.000Z"),
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
await renderPage();
|
|
341
|
+
expect(tabRows("scheduled")).toEqual([
|
|
342
|
+
expect.stringContaining("U2"),
|
|
343
|
+
expect.stringContaining("U1"),
|
|
344
|
+
expect.stringContaining("P2"),
|
|
345
|
+
expect.stringContaining("P1"),
|
|
346
|
+
]);
|
|
297
347
|
});
|
|
298
348
|
|
|
299
|
-
it("
|
|
300
|
-
|
|
349
|
+
it("a kind declaring section purchases lands under Purchases, not Scheduled", async () => {
|
|
350
|
+
const licence: (typeof accountInboxKinds)[number] = {
|
|
351
|
+
kind: "licence",
|
|
352
|
+
label: "purchases.heading",
|
|
353
|
+
section: "purchases",
|
|
354
|
+
load: async () => [{ id: "lic_1", title: "Site licence", at: "2026-09-05T00:00:00.000Z", data: null }],
|
|
355
|
+
renderDetail: () => ({}),
|
|
356
|
+
};
|
|
357
|
+
accountInboxKinds.push(licence);
|
|
358
|
+
try {
|
|
359
|
+
await renderPage();
|
|
360
|
+
expect(tabRows("purchases")).toEqual([
|
|
361
|
+
expect.stringContaining("Site licence"),
|
|
362
|
+
expect.stringContaining("pro"),
|
|
363
|
+
]);
|
|
364
|
+
expect(tabRows("scheduled")).toEqual([]);
|
|
365
|
+
} finally {
|
|
366
|
+
accountInboxKinds.splice(accountInboxKinds.indexOf(licence), 1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("with no rows, every tab still renders and All shows the empty line", async () => {
|
|
371
|
+
mockMemberPurchases.mockResolvedValue({ ok: true, body: { items: [] } });
|
|
301
372
|
await renderPage();
|
|
302
373
|
expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
|
|
303
|
-
"Recents0",
|
|
304
|
-
"Suggested0",
|
|
305
|
-
"Scheduled0",
|
|
306
374
|
"All0",
|
|
375
|
+
"Purchases0",
|
|
376
|
+
"Scheduled0",
|
|
307
377
|
]);
|
|
308
378
|
expect(screen.getByTestId("inbox-empty")).toHaveTextContent("Nothing here yet.");
|
|
309
379
|
});
|
|
@@ -333,7 +403,7 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
|
333
403
|
expect(within(item).getByTestId("for-you-tag").querySelector("svg")).not.toBeNull();
|
|
334
404
|
expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
|
|
335
405
|
"href",
|
|
336
|
-
"/account/
|
|
406
|
+
"/account/general",
|
|
337
407
|
);
|
|
338
408
|
});
|
|
339
409
|
});
|
|
@@ -435,7 +505,7 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
|
435
505
|
|
|
436
506
|
describe("Hub unavailable (D12)", () => {
|
|
437
507
|
it("a failed read renders an error row with Retry, never an empty Inbox", async () => {
|
|
438
|
-
|
|
508
|
+
mockMemberPurchases.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
439
509
|
await renderPage();
|
|
440
510
|
expect(screen.getByTestId("inbox-error")).toHaveTextContent(
|
|
441
511
|
"We couldn't load everything in your Inbox.",
|
|
@@ -465,7 +535,7 @@ describe("Hub unavailable (D12)", () => {
|
|
|
465
535
|
});
|
|
466
536
|
|
|
467
537
|
it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
|
|
468
|
-
|
|
538
|
+
mockMemberPurchases.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
469
539
|
await renderPage("en", "for-you");
|
|
470
540
|
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
471
541
|
expect(within(panel).queryByTestId("inbox-error")).not.toBeInTheDocument();
|
|
@@ -473,6 +543,8 @@ describe("Hub unavailable (D12)", () => {
|
|
|
473
543
|
});
|
|
474
544
|
|
|
475
545
|
describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)", () => {
|
|
546
|
+
beforeEach(memberRows);
|
|
547
|
+
|
|
476
548
|
it("declares piece and message as Inbox kinds in the updates section, labelled from the account namespace", async () => {
|
|
477
549
|
const ctx = await memberCtx();
|
|
478
550
|
expect(kindOf("piece").section).toBe("updates");
|
|
@@ -536,22 +608,39 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
536
608
|
await expect(kindOf(id).load(await memberCtx())).rejects.toMatchObject({ status });
|
|
537
609
|
});
|
|
538
610
|
|
|
539
|
-
it("
|
|
540
|
-
const
|
|
541
|
-
await
|
|
542
|
-
await
|
|
543
|
-
await
|
|
544
|
-
expect(
|
|
545
|
-
expect(
|
|
611
|
+
it("on a cookie-session site both kinds and the source call with the cookie and return rows (#8231 D7)", async () => {
|
|
612
|
+
const cookie = { headers: { cookie: "session=abc" } };
|
|
613
|
+
const ctx = await memberCtx({ init: cookie });
|
|
614
|
+
expect((await kindOf("piece").load(ctx)).map((p) => p.id)).toEqual([PIECE.id, PIECE_NEEDS_CLIENT.id]);
|
|
615
|
+
expect((await kindOf("message").load(ctx)).map((m) => m.id)).toEqual([MESSAGE.id, MESSAGE_WITH_FILE.id]);
|
|
616
|
+
expect((await needsClientSource().load(ctx)).map((i) => i.id)).toEqual([`piece:${PIECE_NEEDS_CLIENT.id}`]);
|
|
617
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(cookie);
|
|
618
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(cookie);
|
|
546
619
|
});
|
|
547
620
|
|
|
548
|
-
it("the cookie-session Inbox
|
|
621
|
+
it("the cookie-session Inbox lists the member rows under All, read with the forwarded cookie (#8231 D7)", async () => {
|
|
549
622
|
await renderPage();
|
|
623
|
+
const sent = expect.objectContaining({ headers: expect.objectContaining({ cookie: "session=abc" }) });
|
|
624
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(sent);
|
|
625
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(sent);
|
|
626
|
+
expect(mockMemberPurchases).toHaveBeenCalledWith(sent);
|
|
627
|
+
expect(mockMemberAppointments).toHaveBeenCalledWith(sent);
|
|
628
|
+
const all = screen.getByTestId("inbox-all");
|
|
629
|
+
expect(within(all).getByText("Spring newsletter")).toBeInTheDocument();
|
|
630
|
+
expect(within(all).getByText("Kickoff notes")).toBeInTheDocument();
|
|
550
631
|
expect(screen.queryByTestId("inbox-error")).not.toBeInTheDocument();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it.each([
|
|
635
|
+
["pieces", mockMemberPieces],
|
|
636
|
+
["messages", mockMemberMessages],
|
|
637
|
+
] as const)("a 500 from member.%s shows the Inbox error row with Retry (#8231 D7)", async (_name, mock) => {
|
|
638
|
+
mock.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
639
|
+
await renderPage();
|
|
640
|
+
const inbox = screen.getByTestId("inbox-panel-inbox");
|
|
641
|
+
expect(within(inbox).getByTestId("inbox-error")).toBeInTheDocument();
|
|
642
|
+
fireEvent.click(within(inbox).getByTestId("inbox-retry"));
|
|
643
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
555
644
|
});
|
|
556
645
|
|
|
557
646
|
it("a piece's detail shows its stage and dates", async () => {
|
|
@@ -598,6 +687,8 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
598
687
|
});
|
|
599
688
|
|
|
600
689
|
describe("the needs-client For-you source (#7920)", () => {
|
|
690
|
+
beforeEach(memberRows);
|
|
691
|
+
|
|
601
692
|
it("a needsClient piece is a needs-you For-you item with an Open action to its Inbox row", async () => {
|
|
602
693
|
const items = await needsClientSource().load(await memberCtx());
|
|
603
694
|
expect(items).toEqual([
|