create-ailk 0.21.0 → 0.22.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/component-catalog.json +37 -3
- package/component-catalog.md +6 -5
- package/dist/add-module.js +9 -5
- package/dist/derive-architecture.d.ts +8 -1
- package/dist/derive-architecture.js +38 -3
- package/dist/design-package.js +7 -2
- package/dist/lib/env-catalog.js +5 -1
- package/dist/module-architecture.js +4 -2
- package/dist/programmatic.js +19 -5
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +39 -18
- package/dist/spoke-checkout.js +177 -236
- package/dist/surfaces.js +19 -14
- package/dist/sync-routes.js +3 -2
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +6 -3
- package/dist/topology.js +25 -1
- package/dist/update.js +5 -0
- package/package.json +3 -3
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/templates/apps/api/CLAUDE.md +15 -6
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
- package/templates/apps/api/src/middleware/auth.ts +13 -6
- package/templates/apps/api/src/openapi/spec.ts +30 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
- package/templates/apps/api/src/routes/consent/account.ts +266 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- package/templates/apps/api/src/routes/content/get-page.ts +4 -0
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +161 -7
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +154 -19
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1038 -118
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -0
- package/templates/apps/api/src/services/lead-routing.ts +337 -8
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- package/templates/apps/api/src/services/person.ts +40 -8
- package/templates/apps/api/src/services/plan-offerings.ts +59 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +8 -1
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/CLAUDE.md +120 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +293 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +115 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +146 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +310 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +143 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +29 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +41 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +438 -31
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +168 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +49 -17
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +106 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- 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]/page.tsx +13 -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]/resources/__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]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +48 -0
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +120 -0
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +86 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +92 -0
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +31 -0
- package/templates/apps/web/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/checkout.test.ts +243 -0
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +5 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +34 -17
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/checkout-config.ts +27 -0
- package/templates/apps/web/lib/checkout.ts +93 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +6 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +447 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +24 -7
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -0
- package/templates/database/CHANGELOG.md +38 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
- package/templates/database/__tests__/seed.test.ts +34 -1
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +34 -3
- package/versions.json +34 -34
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
* unchanged (the caller's own credential); 404 and an unverified 401/403
|
|
24
24
|
* load `[]`, any other failure rejects (D12). On a cookie-session site
|
|
25
25
|
* they call with the cookie too — no gate (#8231 D7).
|
|
26
|
+
* 8. #8765 D8 — the Inbox renders nothing about agreements: the card is
|
|
27
|
+
* retired, whatever `member.consents` answers (the layout's interstitial
|
|
28
|
+
* is the one place an agreement is asked).
|
|
26
29
|
*/
|
|
27
|
-
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
30
|
+
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
28
31
|
|
|
29
32
|
// The global setup mock resolves only top-level namespace keys against
|
|
30
33
|
// en.json; the area reads nested keys (`inbox.tabs.all`, …) — a call
|
|
@@ -34,13 +37,20 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
|
34
37
|
jest.mock("next-intl/server", () => {
|
|
35
38
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
36
39
|
const dig = (obj: unknown, path: string): unknown =>
|
|
37
|
-
path
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
path
|
|
41
|
+
.split(".")
|
|
42
|
+
.reduce<unknown>(
|
|
43
|
+
(acc, k) =>
|
|
44
|
+
acc !== null && typeof acc === "object"
|
|
45
|
+
? (acc as Record<string, unknown>)[k]
|
|
46
|
+
: undefined,
|
|
47
|
+
obj,
|
|
48
|
+
);
|
|
41
49
|
return {
|
|
42
50
|
__esModule: true,
|
|
43
|
-
getTranslations: async (
|
|
51
|
+
getTranslations: async (
|
|
52
|
+
arg: string | { namespace?: string; locale?: string },
|
|
53
|
+
) => {
|
|
44
54
|
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
45
55
|
const messages = dig(msgs, ns);
|
|
46
56
|
return (key: string, values?: Record<string, unknown>): string => {
|
|
@@ -64,7 +74,9 @@ const mockGetSession = jest.fn();
|
|
|
64
74
|
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session fields can mock without standing up Prisma.
|
|
65
75
|
jest.mock("../../../../../auth", () => ({
|
|
66
76
|
__esModule: true,
|
|
67
|
-
auth: {
|
|
77
|
+
auth: {
|
|
78
|
+
api: { getSession: (...args: unknown[]) => mockGetSession(...args) },
|
|
79
|
+
},
|
|
68
80
|
}));
|
|
69
81
|
|
|
70
82
|
const mockSignupMe = jest.fn();
|
|
@@ -73,8 +85,14 @@ const mockMemberPieces = jest.fn();
|
|
|
73
85
|
const mockMemberMessages = jest.fn();
|
|
74
86
|
const mockMemberPurchases = jest.fn();
|
|
75
87
|
const mockMemberAppointments = jest.fn();
|
|
88
|
+
const mockMemberConsents = jest.fn();
|
|
89
|
+
const mockAccountAgreements = jest.fn();
|
|
76
90
|
jest.mock("@working-theory/api-client", () => ({
|
|
77
91
|
__esModule: true,
|
|
92
|
+
// #8765 D7 — the account-level agreements the layout and the page share.
|
|
93
|
+
consent: {
|
|
94
|
+
accountAgreements: (...args: unknown[]) => mockAccountAgreements(...args),
|
|
95
|
+
},
|
|
78
96
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
79
97
|
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
80
98
|
member: {
|
|
@@ -82,12 +100,22 @@ jest.mock("@working-theory/api-client", () => ({
|
|
|
82
100
|
messages: (...args: unknown[]) => mockMemberMessages(...args),
|
|
83
101
|
purchases: (...args: unknown[]) => mockMemberPurchases(...args),
|
|
84
102
|
appointments: (...args: unknown[]) => mockMemberAppointments(...args),
|
|
103
|
+
// #8765 D8 — the retired card's read; the page must not depend on it.
|
|
104
|
+
consents: (...args: unknown[]) => mockMemberConsents(...args),
|
|
85
105
|
// #8233 C4 — the New menu's availability reads; 404 here, so the Inbox
|
|
86
106
|
// cases see no New action and no error row.
|
|
87
107
|
meetingTypes: () =>
|
|
88
|
-
Promise.resolve({
|
|
108
|
+
Promise.resolve({
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 404,
|
|
111
|
+
body: { ok: false, error: { code: "not_found", message: "Not found" } },
|
|
112
|
+
}),
|
|
89
113
|
requestForm: () =>
|
|
90
|
-
Promise.resolve({
|
|
114
|
+
Promise.resolve({
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 404,
|
|
117
|
+
body: { ok: false, error: { code: "not_found", message: "Not found" } },
|
|
118
|
+
}),
|
|
91
119
|
},
|
|
92
120
|
}));
|
|
93
121
|
|
|
@@ -142,14 +170,25 @@ const PURCHASE = {
|
|
|
142
170
|
};
|
|
143
171
|
|
|
144
172
|
function appointment(id: string, title: string, startTime: string) {
|
|
145
|
-
return {
|
|
173
|
+
return {
|
|
174
|
+
id,
|
|
175
|
+
title,
|
|
176
|
+
startTime,
|
|
177
|
+
endTime: startTime,
|
|
178
|
+
status: "confirmed" as const,
|
|
179
|
+
};
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
const NO_APPOINTMENTS = { ok: true, body: { upcoming: [], past: [] } };
|
|
149
183
|
|
|
150
184
|
const NO_SUBSCRIPTION = {
|
|
151
185
|
ok: true,
|
|
152
|
-
body: {
|
|
186
|
+
body: {
|
|
187
|
+
actionRequired: false,
|
|
188
|
+
status: null,
|
|
189
|
+
accessRevokesAt: null,
|
|
190
|
+
portalAvailable: false,
|
|
191
|
+
},
|
|
153
192
|
};
|
|
154
193
|
|
|
155
194
|
const PIECE = {
|
|
@@ -189,9 +228,15 @@ const MESSAGE_WITH_FILE = {
|
|
|
189
228
|
};
|
|
190
229
|
|
|
191
230
|
/** The hub credential a spoke signed in through its hub forwards (#7894 D12). */
|
|
192
|
-
const BEARER = {
|
|
231
|
+
const BEARER = {
|
|
232
|
+
headers: { authorization: `Bearer ailk_hst_${"t".repeat(43)}` },
|
|
233
|
+
};
|
|
193
234
|
|
|
194
|
-
const notFound = {
|
|
235
|
+
const notFound = {
|
|
236
|
+
ok: false,
|
|
237
|
+
status: 404,
|
|
238
|
+
body: { ok: false, error: { code: "not_found", message: "Not found" } },
|
|
239
|
+
};
|
|
195
240
|
|
|
196
241
|
/** The context a hub-mode spoke hands every contribution, signed in. */
|
|
197
242
|
async function memberCtx(
|
|
@@ -200,10 +245,10 @@ async function memberCtx(
|
|
|
200
245
|
const locale = overrides.locale ?? "en";
|
|
201
246
|
// The contract's translator takes any key (account-data.ts's reasoning):
|
|
202
247
|
// forward through the mock's runtime signature.
|
|
203
|
-
const translate = (await getTranslations({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
) => string;
|
|
248
|
+
const translate = (await getTranslations({
|
|
249
|
+
locale,
|
|
250
|
+
namespace: "account",
|
|
251
|
+
})) as (key: string, values?: Record<string, unknown>) => string;
|
|
207
252
|
const t: AccountTranslate = (key, values) => translate(key, values);
|
|
208
253
|
return {
|
|
209
254
|
locale,
|
|
@@ -221,7 +266,9 @@ function kindOf(id: string): (typeof accountInboxKinds)[number] {
|
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
function needsClientSource(): (typeof accountForYouSources)[number] {
|
|
224
|
-
const source = accountForYouSources.find(
|
|
269
|
+
const source = accountForYouSources.find(
|
|
270
|
+
(s) => s.id === "piece-needs-client",
|
|
271
|
+
);
|
|
225
272
|
if (!source) throw new Error("no piece-needs-client source");
|
|
226
273
|
return source;
|
|
227
274
|
}
|
|
@@ -238,7 +285,12 @@ const UNVERIFIED_AND_PAST_DUE = (): void => {
|
|
|
238
285
|
});
|
|
239
286
|
mockSubscriptionMe.mockResolvedValue({
|
|
240
287
|
ok: true,
|
|
241
|
-
body: {
|
|
288
|
+
body: {
|
|
289
|
+
actionRequired: true,
|
|
290
|
+
status: "past_due",
|
|
291
|
+
accessRevokesAt: null,
|
|
292
|
+
portalAvailable: true,
|
|
293
|
+
},
|
|
242
294
|
});
|
|
243
295
|
};
|
|
244
296
|
|
|
@@ -248,24 +300,154 @@ beforeEach(() => {
|
|
|
248
300
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
249
301
|
mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
|
|
250
302
|
mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
|
|
251
|
-
mockMemberPurchases
|
|
303
|
+
mockMemberPurchases
|
|
304
|
+
.mockReset()
|
|
305
|
+
.mockResolvedValue({ ok: true, body: { items: [PURCHASE] } });
|
|
252
306
|
mockMemberAppointments.mockReset().mockResolvedValue(NO_APPOINTMENTS);
|
|
253
307
|
// The page's pieces and messages answer 404 unless a case fills them.
|
|
254
308
|
mockMemberPieces.mockReset().mockResolvedValue(notFound);
|
|
255
309
|
mockMemberMessages.mockReset().mockResolvedValue(notFound);
|
|
310
|
+
mockAccountAgreements
|
|
311
|
+
.mockReset()
|
|
312
|
+
.mockResolvedValue({ ok: true, body: { agreements: [], complete: true } });
|
|
256
313
|
mockRefresh.mockReset();
|
|
257
314
|
});
|
|
258
315
|
|
|
259
316
|
/** The member kinds' own cases read rows from both member reads. */
|
|
260
317
|
function memberRows(): void {
|
|
261
|
-
mockMemberPieces.mockResolvedValue({
|
|
262
|
-
|
|
318
|
+
mockMemberPieces.mockResolvedValue({
|
|
319
|
+
ok: true,
|
|
320
|
+
body: { items: [PIECE, PIECE_NEEDS_CLIENT] },
|
|
321
|
+
});
|
|
322
|
+
mockMemberMessages.mockResolvedValue({
|
|
323
|
+
ok: true,
|
|
324
|
+
body: { items: [MESSAGE, MESSAGE_WITH_FILE] },
|
|
325
|
+
});
|
|
263
326
|
}
|
|
264
327
|
|
|
265
328
|
afterAll(() => {
|
|
266
329
|
accountExperimentKeys.length = 0;
|
|
267
330
|
});
|
|
268
331
|
|
|
332
|
+
describe("no agreements on the Inbox (#8765 D8, AC-3, AC-4)", () => {
|
|
333
|
+
/** A `signUp` agreement the old card would have shown: no grant, or one at an older version. */
|
|
334
|
+
function missingAgreement(
|
|
335
|
+
state: null | { granted: boolean; policyVersion: number },
|
|
336
|
+
) {
|
|
337
|
+
return {
|
|
338
|
+
id: "account_terms",
|
|
339
|
+
kind: "agreement",
|
|
340
|
+
area: null,
|
|
341
|
+
required: true,
|
|
342
|
+
signUp: true,
|
|
343
|
+
description: "Account terms.",
|
|
344
|
+
statement: "I accept the [Terms](terms).",
|
|
345
|
+
policyHref: "/working-agreement",
|
|
346
|
+
policyVersion: 2,
|
|
347
|
+
state: state && {
|
|
348
|
+
...state,
|
|
349
|
+
at: "2026-09-01T00:00:00.000Z",
|
|
350
|
+
source: "sign_up",
|
|
351
|
+
},
|
|
352
|
+
effective: false,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
it.each([
|
|
357
|
+
["no grant", { ok: true, body: { purposes: [missingAgreement(null)] } }],
|
|
358
|
+
[
|
|
359
|
+
"an older version",
|
|
360
|
+
{
|
|
361
|
+
ok: true,
|
|
362
|
+
body: {
|
|
363
|
+
purposes: [missingAgreement({ granted: true, policyVersion: 1 })],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
["a refusal", { ok: false, status: 503, body: {} }],
|
|
368
|
+
])(
|
|
369
|
+
"with member.consents answering %s, no account-agreements element renders",
|
|
370
|
+
async (_label, answer) => {
|
|
371
|
+
mockMemberConsents.mockResolvedValue(answer);
|
|
372
|
+
await renderPage();
|
|
373
|
+
// Let any mount-time effect settle — the retired card appeared only after one.
|
|
374
|
+
await act(async () => {
|
|
375
|
+
for (let i = 0; i < 10; i += 1)
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
377
|
+
});
|
|
378
|
+
expect(screen.getByTestId("account-inbox")).toBeInTheDocument();
|
|
379
|
+
expect(
|
|
380
|
+
document.querySelector('[data-testid^="account-agreements"]'),
|
|
381
|
+
).toBeNull();
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("no Inbox loader runs behind the interstitial (#8765 D7)", () => {
|
|
387
|
+
const OUTSTANDING = {
|
|
388
|
+
ok: true,
|
|
389
|
+
body: {
|
|
390
|
+
agreements: [
|
|
391
|
+
{
|
|
392
|
+
id: "account_terms",
|
|
393
|
+
statement: "I accept the [Terms](terms).",
|
|
394
|
+
policyVersion: 2,
|
|
395
|
+
acceptedVersion: null,
|
|
396
|
+
acceptedAt: null,
|
|
397
|
+
current: false,
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
complete: false,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const inboxLoaders = () => [
|
|
405
|
+
mockMemberPieces,
|
|
406
|
+
mockMemberMessages,
|
|
407
|
+
mockMemberPurchases,
|
|
408
|
+
mockMemberAppointments,
|
|
409
|
+
mockSubscriptionMe,
|
|
410
|
+
mockSignupMe,
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
it.each([
|
|
414
|
+
[
|
|
415
|
+
"complete: false",
|
|
416
|
+
() => mockAccountAgreements.mockResolvedValue(OUTSTANDING),
|
|
417
|
+
],
|
|
418
|
+
[
|
|
419
|
+
"a 503 (fails closed)",
|
|
420
|
+
() =>
|
|
421
|
+
mockAccountAgreements.mockResolvedValue({
|
|
422
|
+
ok: false,
|
|
423
|
+
status: 503,
|
|
424
|
+
body: {},
|
|
425
|
+
}),
|
|
426
|
+
],
|
|
427
|
+
[
|
|
428
|
+
"a thrown read (fails closed)",
|
|
429
|
+
() => mockAccountAgreements.mockRejectedValue(new Error("down")),
|
|
430
|
+
],
|
|
431
|
+
])(
|
|
432
|
+
"with %s, the page renders nothing and calls no Inbox loader",
|
|
433
|
+
async (_label, arrange) => {
|
|
434
|
+
arrange();
|
|
435
|
+
const element = await Page({ params: Promise.resolve({ locale: "en" }) });
|
|
436
|
+
expect(element).toBeNull();
|
|
437
|
+
// member.messages stamps readAt on its first read: it must never run here.
|
|
438
|
+
for (const loader of inboxLoaders())
|
|
439
|
+
expect(loader).not.toHaveBeenCalled();
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
it("with an older hub's 404, the Inbox loads as before", async () => {
|
|
444
|
+
mockAccountAgreements.mockResolvedValue(notFound);
|
|
445
|
+
await renderPage();
|
|
446
|
+
expect(screen.getByTestId("account-inbox")).toBeInTheDocument();
|
|
447
|
+
expect(mockMemberMessages).toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
269
451
|
describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", () => {
|
|
270
452
|
/** The rows of one tab, by title, after selecting it. */
|
|
271
453
|
function tabRows(id: string): string[] {
|
|
@@ -278,23 +460,42 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
|
|
|
278
460
|
it("renders All · Purchases · Scheduled with counts, All selected — no Recents or Suggested", async () => {
|
|
279
461
|
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
280
462
|
mockMemberPieces.mockResolvedValue({ ok: true, body: { items: [PIECE] } });
|
|
281
|
-
mockMemberMessages.mockResolvedValue({
|
|
463
|
+
mockMemberMessages.mockResolvedValue({
|
|
464
|
+
ok: true,
|
|
465
|
+
body: { items: [MESSAGE] },
|
|
466
|
+
});
|
|
282
467
|
mockMemberAppointments.mockResolvedValue({
|
|
283
468
|
ok: true,
|
|
284
|
-
body: {
|
|
469
|
+
body: {
|
|
470
|
+
upcoming: [
|
|
471
|
+
appointment("appt_1", "Kickoff call", "2026-09-10T15:00:00.000Z"),
|
|
472
|
+
],
|
|
473
|
+
past: [],
|
|
474
|
+
},
|
|
285
475
|
});
|
|
286
476
|
await renderPage();
|
|
287
|
-
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
477
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
478
|
+
"Inbox",
|
|
479
|
+
);
|
|
288
480
|
const tabs = screen.getAllByRole("tab");
|
|
289
481
|
expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
|
|
290
482
|
"inbox-tab-all",
|
|
291
483
|
"inbox-tab-purchases",
|
|
292
484
|
"inbox-tab-scheduled",
|
|
293
485
|
]);
|
|
294
|
-
expect(tabs.map((tab) => tab.textContent)).toEqual([
|
|
295
|
-
|
|
486
|
+
expect(tabs.map((tab) => tab.textContent)).toEqual([
|
|
487
|
+
"All5",
|
|
488
|
+
"Purchases1",
|
|
489
|
+
"Scheduled1",
|
|
490
|
+
]);
|
|
491
|
+
expect(screen.getByTestId("inbox-tab-all")).toHaveAttribute(
|
|
492
|
+
"aria-selected",
|
|
493
|
+
"true",
|
|
494
|
+
);
|
|
296
495
|
// All: every row, newest first.
|
|
297
|
-
const all = within(screen.getByTestId("inbox-all")).getAllByTestId(
|
|
496
|
+
const all = within(screen.getByTestId("inbox-all")).getAllByTestId(
|
|
497
|
+
"inbox-all-row",
|
|
498
|
+
);
|
|
298
499
|
expect(all.map((row) => row.textContent)).toEqual([
|
|
299
500
|
expect.stringContaining("Kickoff call"),
|
|
300
501
|
expect.stringContaining("Kickoff notes"),
|
|
@@ -304,9 +505,13 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
|
|
|
304
505
|
]);
|
|
305
506
|
// Purchases: the purchase row only. Scheduled: the appointment only.
|
|
306
507
|
expect(tabRows("purchases")).toEqual([expect.stringContaining("pro")]);
|
|
307
|
-
expect(tabRows("scheduled")).toEqual([
|
|
508
|
+
expect(tabRows("scheduled")).toEqual([
|
|
509
|
+
expect.stringContaining("Kickoff call"),
|
|
510
|
+
]);
|
|
308
511
|
// The result row is under All and in no other tab.
|
|
309
|
-
expect(
|
|
512
|
+
expect(
|
|
513
|
+
screen.queryByText("ailk-founder-diagnostic"),
|
|
514
|
+
).not.toBeInTheDocument();
|
|
310
515
|
});
|
|
311
516
|
|
|
312
517
|
it("each All row links to its item page, prefixed off the default locale", async () => {
|
|
@@ -319,7 +524,10 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
|
|
|
319
524
|
);
|
|
320
525
|
expect(
|
|
321
526
|
within(all).getByText("ailk-founder-diagnostic").closest("a"),
|
|
322
|
-
).toHaveAttribute(
|
|
527
|
+
).toHaveAttribute(
|
|
528
|
+
"href",
|
|
529
|
+
"/fr/account/inbox/result/ailk-founder-diagnostic",
|
|
530
|
+
);
|
|
323
531
|
});
|
|
324
532
|
|
|
325
533
|
it("Scheduled lists the upcoming soonest first, then the past newest first", async () => {
|
|
@@ -351,7 +559,14 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
|
|
|
351
559
|
kind: "licence",
|
|
352
560
|
label: "purchases.heading",
|
|
353
561
|
section: "purchases",
|
|
354
|
-
load: async () => [
|
|
562
|
+
load: async () => [
|
|
563
|
+
{
|
|
564
|
+
id: "lic_1",
|
|
565
|
+
title: "Site licence",
|
|
566
|
+
at: "2026-09-05T00:00:00.000Z",
|
|
567
|
+
data: null,
|
|
568
|
+
},
|
|
569
|
+
],
|
|
355
570
|
renderDetail: () => ({}),
|
|
356
571
|
};
|
|
357
572
|
accountInboxKinds.push(licence);
|
|
@@ -375,7 +590,9 @@ describe("the Inbox — tabs All · Purchases · Scheduled (#8231 D3, AC-3)", ()
|
|
|
375
590
|
"Purchases0",
|
|
376
591
|
"Scheduled0",
|
|
377
592
|
]);
|
|
378
|
-
expect(screen.getByTestId("inbox-empty")).toHaveTextContent(
|
|
593
|
+
expect(screen.getByTestId("inbox-empty")).toHaveTextContent(
|
|
594
|
+
"Nothing here yet.",
|
|
595
|
+
);
|
|
379
596
|
});
|
|
380
597
|
});
|
|
381
598
|
|
|
@@ -386,7 +603,9 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
|
386
603
|
const forYou = screen.getByTestId("inbox-panel-for-you");
|
|
387
604
|
expect(within(forYou).getAllByTestId("for-you-item")).toHaveLength(2);
|
|
388
605
|
const list = screen.getByTestId("inbox-list");
|
|
389
|
-
expect(
|
|
606
|
+
expect(
|
|
607
|
+
within(list).queryByText("Email not verified"),
|
|
608
|
+
).not.toBeInTheDocument();
|
|
390
609
|
expect(
|
|
391
610
|
within(list).queryByText("Your payment didn't go through"),
|
|
392
611
|
).not.toBeInTheDocument();
|
|
@@ -400,7 +619,9 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
|
400
619
|
expect(item).toHaveAttribute("data-item-id", "add-name");
|
|
401
620
|
expect(item).toHaveTextContent("Add your name");
|
|
402
621
|
// DESIGN.md §5.4c — the tag is icon + label.
|
|
403
|
-
expect(
|
|
622
|
+
expect(
|
|
623
|
+
within(item).getByTestId("for-you-tag").querySelector("svg"),
|
|
624
|
+
).not.toBeNull();
|
|
404
625
|
expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
|
|
405
626
|
"href",
|
|
406
627
|
"/account/general",
|
|
@@ -424,15 +645,22 @@ describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
|
424
645
|
UNVERIFIED_AND_PAST_DUE();
|
|
425
646
|
await renderPage();
|
|
426
647
|
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("hidden");
|
|
427
|
-
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass(
|
|
648
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass(
|
|
649
|
+
"md:hidden",
|
|
650
|
+
);
|
|
428
651
|
expect(screen.getByTestId("inbox-panel-for-you")).toHaveClass("hidden");
|
|
429
652
|
});
|
|
430
653
|
|
|
431
654
|
it("the phone has no large title or lead — both stay at md+", async () => {
|
|
432
655
|
await renderPage();
|
|
433
|
-
expect(screen.getByRole("heading", { level: 1 })).toHaveClass(
|
|
656
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveClass(
|
|
657
|
+
"max-md:sr-only",
|
|
658
|
+
);
|
|
434
659
|
expect(screen.getByRole("heading", { level: 1 })).not.toHaveClass("hidden");
|
|
435
|
-
expect(screen.getByText("What changed, and what's coming.")).toHaveClass(
|
|
660
|
+
expect(screen.getByText("What changed, and what's coming.")).toHaveClass(
|
|
661
|
+
"hidden",
|
|
662
|
+
"md:block",
|
|
663
|
+
);
|
|
436
664
|
});
|
|
437
665
|
|
|
438
666
|
it("the list sits in a bordered card, inset by the canvas on every side", async () => {
|
|
@@ -442,7 +670,9 @@ describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
|
442
670
|
// no full-bleed escape, and the list is RowGroup's bordered box.
|
|
443
671
|
const page = screen.getByTestId("account-inbox");
|
|
444
672
|
expect(page.className).not.toMatch(/-mx-|w-screen/);
|
|
445
|
-
expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(
|
|
673
|
+
expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(
|
|
674
|
+
/-mx-|w-screen/,
|
|
675
|
+
);
|
|
446
676
|
});
|
|
447
677
|
});
|
|
448
678
|
|
|
@@ -458,20 +688,28 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
|
458
688
|
expect(panel).toHaveClass("md:hidden");
|
|
459
689
|
expect(panel).not.toHaveClass("hidden");
|
|
460
690
|
// The Inbox panel is hidden on the phone but still shows at md+.
|
|
461
|
-
expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass(
|
|
691
|
+
expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass(
|
|
692
|
+
"hidden",
|
|
693
|
+
"md:block",
|
|
694
|
+
);
|
|
462
695
|
const card = within(panel).getByTestId("for-you-list-card");
|
|
463
696
|
const items = within(card).getAllByTestId("for-you-item");
|
|
464
697
|
expect(items).toHaveLength(3);
|
|
465
698
|
for (const item of items) {
|
|
466
699
|
expect(within(item).getByTestId("for-you-tag")).toBeInTheDocument();
|
|
467
700
|
expect(
|
|
468
|
-
within(item).queryByTestId("for-you-open") ??
|
|
701
|
+
within(item).queryByTestId("for-you-open") ??
|
|
702
|
+
within(item).queryByTestId("for-you-view"),
|
|
469
703
|
).not.toBeNull();
|
|
470
|
-
expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(
|
|
704
|
+
expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(
|
|
705
|
+
/^Dismiss/,
|
|
706
|
+
);
|
|
471
707
|
}
|
|
472
708
|
expect(within(panel).getByText("Add your name")).toBeInTheDocument();
|
|
473
709
|
expect(within(panel).getByText("Email not verified")).toBeInTheDocument();
|
|
474
|
-
expect(
|
|
710
|
+
expect(
|
|
711
|
+
within(panel).getByText("Your payment didn't go through"),
|
|
712
|
+
).toBeInTheDocument();
|
|
475
713
|
});
|
|
476
714
|
|
|
477
715
|
it("✕ dismisses an item — it leaves the list", async () => {
|
|
@@ -483,7 +721,9 @@ describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
|
483
721
|
fireEvent.click(within(first!).getByTestId("for-you-dismiss"));
|
|
484
722
|
const left = within(panel).getAllByTestId("for-you-item");
|
|
485
723
|
expect(left).toHaveLength(1);
|
|
486
|
-
expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(
|
|
724
|
+
expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(
|
|
725
|
+
id,
|
|
726
|
+
);
|
|
487
727
|
});
|
|
488
728
|
|
|
489
729
|
it("the For you tab keeps the page's h1 (visually hidden on the phone)", async () => {
|
|
@@ -519,7 +759,9 @@ describe("Hub unavailable (D12)", () => {
|
|
|
519
759
|
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
520
760
|
await renderPage();
|
|
521
761
|
expect(
|
|
522
|
-
within(screen.getByTestId("inbox-panel-inbox")).getByTestId(
|
|
762
|
+
within(screen.getByTestId("inbox-panel-inbox")).getByTestId(
|
|
763
|
+
"inbox-error",
|
|
764
|
+
),
|
|
523
765
|
).toBeInTheDocument();
|
|
524
766
|
});
|
|
525
767
|
|
|
@@ -531,7 +773,9 @@ describe("Hub unavailable (D12)", () => {
|
|
|
531
773
|
"We couldn't load everything in your Inbox.",
|
|
532
774
|
);
|
|
533
775
|
// Never a silently empty list (D12): the empty line gives way to the error row.
|
|
534
|
-
expect(
|
|
776
|
+
expect(
|
|
777
|
+
within(panel).queryByTestId("for-you-empty"),
|
|
778
|
+
).not.toBeInTheDocument();
|
|
535
779
|
});
|
|
536
780
|
|
|
537
781
|
it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
|
|
@@ -569,24 +813,36 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
569
813
|
]);
|
|
570
814
|
});
|
|
571
815
|
|
|
572
|
-
it.each(["piece", "message"])(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
816
|
+
it.each(["piece", "message"])(
|
|
817
|
+
"%s loads [] on a 404 — nothing for this caller",
|
|
818
|
+
async (id) => {
|
|
819
|
+
mockMemberPieces.mockResolvedValue(notFound);
|
|
820
|
+
mockMemberMessages.mockResolvedValue(notFound);
|
|
821
|
+
await expect(kindOf(id).load(await memberCtx())).resolves.toEqual([]);
|
|
822
|
+
},
|
|
823
|
+
);
|
|
577
824
|
|
|
578
825
|
it.each([
|
|
579
826
|
["piece", 401],
|
|
580
827
|
["piece", 403],
|
|
581
828
|
["message", 401],
|
|
582
829
|
["message", 403],
|
|
583
|
-
])(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
830
|
+
])(
|
|
831
|
+
"%s loads [] on a %i while the email is unverified",
|
|
832
|
+
async (id, status) => {
|
|
833
|
+
const refusal = {
|
|
834
|
+
ok: false,
|
|
835
|
+
status,
|
|
836
|
+
body: { ok: false, error: { code: "email_unverified" } },
|
|
837
|
+
};
|
|
838
|
+
mockMemberPieces.mockResolvedValue(refusal);
|
|
839
|
+
mockMemberMessages.mockResolvedValue(refusal);
|
|
840
|
+
const ctx = await memberCtx({
|
|
841
|
+
user: { name: "Ada", email: "ada@example.com", emailVerified: false },
|
|
842
|
+
});
|
|
843
|
+
await expect(kindOf(id).load(ctx)).resolves.toEqual([]);
|
|
844
|
+
},
|
|
845
|
+
);
|
|
590
846
|
|
|
591
847
|
it.each([
|
|
592
848
|
["piece", 401],
|
|
@@ -597,30 +853,51 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
597
853
|
["message", 403],
|
|
598
854
|
["message", 502],
|
|
599
855
|
["message", 0],
|
|
600
|
-
])(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
856
|
+
])(
|
|
857
|
+
"%s rejects on a %i for a verified email — never a partial list shown as whole",
|
|
858
|
+
async (id, status) => {
|
|
859
|
+
const failure = {
|
|
860
|
+
ok: false,
|
|
861
|
+
status,
|
|
862
|
+
body: {
|
|
863
|
+
ok: false,
|
|
864
|
+
error: {
|
|
865
|
+
code: status === 0 ? "insecure_transport" : "failed",
|
|
866
|
+
message: "x",
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
};
|
|
870
|
+
mockMemberPieces.mockResolvedValue(failure);
|
|
871
|
+
mockMemberMessages.mockResolvedValue(failure);
|
|
872
|
+
await expect(kindOf(id).load(await memberCtx())).rejects.toMatchObject({
|
|
873
|
+
status,
|
|
874
|
+
});
|
|
875
|
+
},
|
|
876
|
+
);
|
|
610
877
|
|
|
611
878
|
it("on a cookie-session site both kinds and the source call with the cookie and return rows (#8231 D7)", async () => {
|
|
612
879
|
const cookie = { headers: { cookie: "session=abc" } };
|
|
613
880
|
const ctx = await memberCtx({ init: cookie });
|
|
614
|
-
expect((await kindOf("piece").load(ctx)).map((p) => p.id)).toEqual([
|
|
615
|
-
|
|
616
|
-
|
|
881
|
+
expect((await kindOf("piece").load(ctx)).map((p) => p.id)).toEqual([
|
|
882
|
+
PIECE.id,
|
|
883
|
+
PIECE_NEEDS_CLIENT.id,
|
|
884
|
+
]);
|
|
885
|
+
expect((await kindOf("message").load(ctx)).map((m) => m.id)).toEqual([
|
|
886
|
+
MESSAGE.id,
|
|
887
|
+
MESSAGE_WITH_FILE.id,
|
|
888
|
+
]);
|
|
889
|
+
expect((await needsClientSource().load(ctx)).map((i) => i.id)).toEqual([
|
|
890
|
+
`piece:${PIECE_NEEDS_CLIENT.id}`,
|
|
891
|
+
]);
|
|
617
892
|
expect(mockMemberPieces).toHaveBeenCalledWith(cookie);
|
|
618
893
|
expect(mockMemberMessages).toHaveBeenCalledWith(cookie);
|
|
619
894
|
});
|
|
620
895
|
|
|
621
896
|
it("the cookie-session Inbox lists the member rows under All, read with the forwarded cookie (#8231 D7)", async () => {
|
|
622
897
|
await renderPage();
|
|
623
|
-
const sent = expect.objectContaining({
|
|
898
|
+
const sent = expect.objectContaining({
|
|
899
|
+
headers: expect.objectContaining({ cookie: "session=abc" }),
|
|
900
|
+
});
|
|
624
901
|
expect(mockMemberPieces).toHaveBeenCalledWith(sent);
|
|
625
902
|
expect(mockMemberMessages).toHaveBeenCalledWith(sent);
|
|
626
903
|
expect(mockMemberPurchases).toHaveBeenCalledWith(sent);
|
|
@@ -634,14 +911,17 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
634
911
|
it.each([
|
|
635
912
|
["pieces", mockMemberPieces],
|
|
636
913
|
["messages", mockMemberMessages],
|
|
637
|
-
] as const)(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
914
|
+
] as const)(
|
|
915
|
+
"a 500 from member.%s shows the Inbox error row with Retry (#8231 D7)",
|
|
916
|
+
async (_name, mock) => {
|
|
917
|
+
mock.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
918
|
+
await renderPage();
|
|
919
|
+
const inbox = screen.getByTestId("inbox-panel-inbox");
|
|
920
|
+
expect(within(inbox).getByTestId("inbox-error")).toBeInTheDocument();
|
|
921
|
+
fireEvent.click(within(inbox).getByTestId("inbox-retry"));
|
|
922
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
923
|
+
},
|
|
924
|
+
);
|
|
645
925
|
|
|
646
926
|
it("a piece's detail shows its stage and dates", async () => {
|
|
647
927
|
const ctx = await memberCtx();
|
|
@@ -649,7 +929,11 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
649
929
|
const detail = kindOf("piece").renderDetail(first!, ctx);
|
|
650
930
|
expect(detail.status).toBe("Drafting");
|
|
651
931
|
expect(detail.fields?.heading).toBe("My pieces");
|
|
652
|
-
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
932
|
+
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
933
|
+
"Stage",
|
|
934
|
+
"Started",
|
|
935
|
+
"Updated",
|
|
936
|
+
]);
|
|
653
937
|
});
|
|
654
938
|
|
|
655
939
|
it("a message with an asset shows a file action that opens the member file read", async () => {
|
|
@@ -665,16 +949,19 @@ describe("the member kinds — piece and message (#7920, spec 7423 D8 · D12)",
|
|
|
665
949
|
href: `/account/inbox/message/${MESSAGE_WITH_FILE.id}/file`,
|
|
666
950
|
},
|
|
667
951
|
]);
|
|
668
|
-
expect(detail.fields?.items).toContainEqual({
|
|
952
|
+
expect(detail.fields?.items).toContainEqual({
|
|
953
|
+
label: "File",
|
|
954
|
+
value: "brand-guide.pdf",
|
|
955
|
+
});
|
|
669
956
|
});
|
|
670
957
|
|
|
671
958
|
it("the file action carries the locale prefix off the default locale", async () => {
|
|
672
959
|
const ctx = await memberCtx({ locale: "fr" });
|
|
673
960
|
const items = await kindOf("message").load(ctx);
|
|
674
961
|
const withFile = items.find((m) => m.id === MESSAGE_WITH_FILE.id)!;
|
|
675
|
-
expect(
|
|
676
|
-
|
|
677
|
-
);
|
|
962
|
+
expect(
|
|
963
|
+
kindOf("message").renderDetail(withFile, ctx).actions?.[0]?.href,
|
|
964
|
+
).toBe(`/fr/account/inbox/message/${MESSAGE_WITH_FILE.id}/file`);
|
|
678
965
|
});
|
|
679
966
|
|
|
680
967
|
it("a message without an asset has no file action", async () => {
|
|
@@ -697,7 +984,10 @@ describe("the needs-client For-you source (#7920)", () => {
|
|
|
697
984
|
type: { id: "piece", label: "Piece", tone: "needs-you" },
|
|
698
985
|
title: "About page",
|
|
699
986
|
time: { kind: "at", at: PIECE_NEEDS_CLIENT.updatedAt },
|
|
700
|
-
action: {
|
|
987
|
+
action: {
|
|
988
|
+
kind: "open",
|
|
989
|
+
href: `/account/inbox/piece/${PIECE_NEEDS_CLIENT.id}`,
|
|
990
|
+
},
|
|
701
991
|
dismissible: true,
|
|
702
992
|
},
|
|
703
993
|
]);
|
|
@@ -705,7 +995,9 @@ describe("the needs-client For-you source (#7920)", () => {
|
|
|
705
995
|
});
|
|
706
996
|
|
|
707
997
|
it("off the default locale the Open href carries the prefix", async () => {
|
|
708
|
-
const [item] = await needsClientSource().load(
|
|
998
|
+
const [item] = await needsClientSource().load(
|
|
999
|
+
await memberCtx({ locale: "fr" }),
|
|
1000
|
+
);
|
|
709
1001
|
expect(item?.action).toEqual({
|
|
710
1002
|
kind: "open",
|
|
711
1003
|
href: `/fr/account/inbox/piece/${PIECE_NEEDS_CLIENT.id}`,
|
|
@@ -714,8 +1006,12 @@ describe("the needs-client For-you source (#7920)", () => {
|
|
|
714
1006
|
|
|
715
1007
|
it("loads [] on a 404 and rejects on any other failure", async () => {
|
|
716
1008
|
mockMemberPieces.mockResolvedValue(notFound);
|
|
717
|
-
await expect(needsClientSource().load(await memberCtx())).resolves.toEqual(
|
|
1009
|
+
await expect(needsClientSource().load(await memberCtx())).resolves.toEqual(
|
|
1010
|
+
[],
|
|
1011
|
+
);
|
|
718
1012
|
mockMemberPieces.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
719
|
-
await expect(
|
|
1013
|
+
await expect(
|
|
1014
|
+
needsClientSource().load(await memberCtx()),
|
|
1015
|
+
).rejects.toMatchObject({ status: 500 });
|
|
720
1016
|
});
|
|
721
1017
|
});
|