create-ailk 0.11.2 → 0.11.3
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 +12 -1
- package/component-catalog.md +6 -5
- package/dist/surfaces.d.ts +4 -5
- package/dist/surfaces.js +50 -8
- package/package.json +3 -3
- package/templates/.env.example +29 -0
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +135 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +233 -12
- package/templates/apps/api/src/lib/auth-email-budget.ts +3 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +529 -1
- package/templates/apps/api/src/middleware/rate-limit.ts +341 -28
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +105 -1
- package/templates/apps/api/src/routes/auth/transactional-email.ts +37 -9
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +251 -0
- package/templates/apps/api/src/services/lead-accept-user.ts +126 -0
- package/templates/apps/api/src/vercel-handler.ts +58 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +81 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +4 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +3 -1
- package/templates/apps/web/.env.example +4 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +2 -1
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +14 -9
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +5 -4
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +69 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +156 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +44 -26
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +104 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +57 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +306 -28
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +143 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +297 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +145 -234
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +183 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +143 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +3 -1
- package/templates/apps/web/app/[locale]/(authed)/account/error-row.tsx +35 -0
- package/templates/apps/web/app/[locale]/(authed)/account/for-you-labels.ts +36 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/[kind]/[id]/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +88 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +120 -297
- package/templates/apps/web/app/[locale]/(authed)/account/retry-button.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +68 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +55 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +118 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +298 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +32 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support/support-form.tsx +98 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +98 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +77 -3
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +52 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +63 -2
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +22 -2
- package/templates/apps/web/app/[locale]/layout.tsx +41 -28
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +66 -2
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +22 -6
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +176 -0
- package/templates/apps/web/app/openapi.json/route.ts +127 -0
- package/templates/apps/web/auth.ts +29 -5
- package/templates/apps/web/jest.config.cjs +5 -0
- package/templates/apps/web/lib/__tests__/account-config.test.ts +285 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +156 -1
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +41 -2
- package/templates/apps/web/lib/account-config.ts +219 -11
- package/templates/apps/web/lib/auth-email.ts +104 -2
- package/templates/apps/web/lib/auth-env.ts +37 -5
- package/templates/apps/web/lib/auth-routes.ts +33 -4
- package/templates/apps/web/messages/en.json +147 -21
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +0 -39
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for /[locale]/account — the
|
|
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
4
|
*
|
|
4
5
|
* Covers:
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* 1. The D6a tabs Recents · Suggested · Scheduled · All, each with its
|
|
7
|
+
* count (zeros included), Recents selected.
|
|
8
|
+
* 2. `result` and `purchase` rows sit under Recents and All, newest first,
|
|
9
|
+
* each linking to its item page.
|
|
10
|
+
* 3. `email-unverified` and `payment-due` are For-you items (the phone
|
|
11
|
+
* stack), never Inbox rows.
|
|
12
|
+
* 4. A member with no name gets an "Add your name" For-you item (needs-you,
|
|
13
|
+
* an Open action) linking to User settings › General.
|
|
14
|
+
* 5. A failed read renders an error row with Retry — never an empty Inbox.
|
|
11
15
|
*/
|
|
12
|
-
import { fireEvent, render, screen,
|
|
16
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
13
17
|
|
|
14
18
|
// The global setup mock resolves only top-level namespace keys against
|
|
15
|
-
// en.json;
|
|
16
|
-
//
|
|
17
|
-
//
|
|
19
|
+
// en.json; the area reads nested keys (`inbox.tabs.recents`, …) — a call
|
|
20
|
+
// shape real next-intl supports — so resolve dotted namespaces AND dotted
|
|
21
|
+
// keys here against the same catalog. `{name}` placeholders are filled from
|
|
22
|
+
// `values` the way next-intl does.
|
|
18
23
|
jest.mock("next-intl/server", () => {
|
|
19
24
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
20
25
|
const dig = (obj: unknown, path: string): unknown =>
|
|
@@ -27,9 +32,12 @@ jest.mock("next-intl/server", () => {
|
|
|
27
32
|
getTranslations: async (arg: string | { namespace?: string; locale?: string }) => {
|
|
28
33
|
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
29
34
|
const messages = dig(msgs, ns);
|
|
30
|
-
return (key: string): string => {
|
|
35
|
+
return (key: string, values?: Record<string, unknown>): string => {
|
|
31
36
|
const value = dig(messages, key);
|
|
32
|
-
|
|
37
|
+
if (typeof value !== "string") return `${ns}.${key}`;
|
|
38
|
+
return value.replace(/\{(\w+)\}/g, (m, name: string) =>
|
|
39
|
+
values && name in values ? String(values[name]) : m,
|
|
40
|
+
);
|
|
33
41
|
};
|
|
34
42
|
},
|
|
35
43
|
};
|
|
@@ -42,7 +50,7 @@ jest.mock("next/headers", () => ({
|
|
|
42
50
|
}));
|
|
43
51
|
|
|
44
52
|
const mockGetSession = jest.fn();
|
|
45
|
-
// 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
|
|
53
|
+
// 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.
|
|
46
54
|
jest.mock("../../../../../auth", () => ({
|
|
47
55
|
__esModule: true,
|
|
48
56
|
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
@@ -51,44 +59,31 @@ jest.mock("../../../../../auth", () => ({
|
|
|
51
59
|
const mockSignupMe = jest.fn();
|
|
52
60
|
const mockCheckoutsMe = jest.fn();
|
|
53
61
|
const mockSubscriptionMe = jest.fn();
|
|
54
|
-
const mockSubscriptionPortalSession = jest.fn();
|
|
55
62
|
jest.mock("@working-theory/api-client", () => ({
|
|
56
63
|
__esModule: true,
|
|
57
64
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
58
65
|
flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
|
|
59
|
-
subscription: {
|
|
60
|
-
me: (...args: unknown[]) => mockSubscriptionMe(...args),
|
|
61
|
-
portalSession: (...args: unknown[]) => mockSubscriptionPortalSession(...args),
|
|
62
|
-
},
|
|
63
|
-
}));
|
|
64
|
-
|
|
65
|
-
let mockExperimentKeys: string[] = [];
|
|
66
|
-
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; account-config.ts is the OSS template's (empty) experiment registry — a pure constant with no external boundary to mock instead, overridden here to exercise the populated path.
|
|
67
|
-
jest.mock("../../../../../lib/account-config", () => ({
|
|
68
|
-
__esModule: true,
|
|
69
|
-
get accountExperimentKeys() {
|
|
70
|
-
return mockExperimentKeys;
|
|
71
|
-
},
|
|
66
|
+
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
72
67
|
}));
|
|
73
68
|
|
|
74
|
-
const mockSignOut = jest.fn();
|
|
75
|
-
jest.mock("better-auth/react", () => ({
|
|
76
|
-
__esModule: true,
|
|
77
|
-
createAuthClient: () => ({ signOut: (...args: unknown[]) => mockSignOut(...args) }),
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
const mockPush = jest.fn();
|
|
81
69
|
const mockRefresh = jest.fn();
|
|
82
70
|
jest.mock("next/navigation", () => ({
|
|
83
71
|
__esModule: true,
|
|
84
|
-
useRouter: () => ({ push:
|
|
85
|
-
usePathname: () => "/
|
|
72
|
+
useRouter: () => ({ push: jest.fn(), refresh: mockRefresh }),
|
|
73
|
+
usePathname: () => "/account",
|
|
86
74
|
}));
|
|
87
75
|
|
|
76
|
+
import { accountExperimentKeys } from "../../../../../lib/account-config";
|
|
88
77
|
import Page from "../page";
|
|
89
78
|
|
|
90
79
|
const SESSION = {
|
|
91
|
-
user: {
|
|
80
|
+
user: {
|
|
81
|
+
id: "user_1",
|
|
82
|
+
name: "Ada",
|
|
83
|
+
email: "ada@example.com",
|
|
84
|
+
emailVerified: true,
|
|
85
|
+
createdAt: new Date("2026-09-01T00:00:00.000Z"),
|
|
86
|
+
},
|
|
92
87
|
};
|
|
93
88
|
|
|
94
89
|
const SIGNUP = {
|
|
@@ -101,244 +96,160 @@ const SIGNUP = {
|
|
|
101
96
|
answers: { q3: null },
|
|
102
97
|
};
|
|
103
98
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
const CHECKOUT = {
|
|
100
|
+
id: "fc_done",
|
|
101
|
+
flowSlug: "founder-flow",
|
|
102
|
+
experimentKey: "ailk-founder-diagnostic",
|
|
103
|
+
offerKey: "pro",
|
|
104
|
+
status: "completed",
|
|
105
|
+
amountMinor: 4900,
|
|
106
|
+
currency: "usd",
|
|
107
|
+
period: "monthly",
|
|
108
|
+
completedAt: "2026-09-04T10:00:00.000Z",
|
|
109
|
+
createdAt: "2026-09-04T09:58:00.000Z",
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
experimentKey: "ailk-founder-diagnostic",
|
|
115
|
-
offerKey: "pro",
|
|
116
|
-
status: "completed",
|
|
117
|
-
amountMinor: 4900,
|
|
118
|
-
currency: "usd",
|
|
119
|
-
period: "monthly",
|
|
120
|
-
completedAt: "2026-09-04T10:00:00.000Z",
|
|
121
|
-
createdAt: "2026-09-04T09:58:00.000Z",
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: "fc_open",
|
|
125
|
-
flowSlug: "founder-flow",
|
|
126
|
-
experimentKey: "ailk-founder-diagnostic",
|
|
127
|
-
offerKey: "starter",
|
|
128
|
-
status: "created",
|
|
129
|
-
amountMinor: 1900,
|
|
130
|
-
currency: "usd",
|
|
131
|
-
period: "monthly",
|
|
132
|
-
completedAt: null,
|
|
133
|
-
createdAt: "2026-09-05T08:00:00.000Z",
|
|
134
|
-
},
|
|
135
|
-
];
|
|
112
|
+
const NO_SUBSCRIPTION = {
|
|
113
|
+
ok: true,
|
|
114
|
+
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
115
|
+
};
|
|
136
116
|
|
|
137
|
-
async function renderPage(): Promise<void> {
|
|
138
|
-
const element = await Page({ params: Promise.resolve({ locale
|
|
117
|
+
async function renderPage(locale = "en"): Promise<void> {
|
|
118
|
+
const element = await Page({ params: Promise.resolve({ locale }) });
|
|
139
119
|
render(element);
|
|
140
120
|
}
|
|
141
121
|
|
|
142
122
|
beforeEach(() => {
|
|
123
|
+
accountExperimentKeys.length = 0;
|
|
143
124
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
144
|
-
mockSignupMe.mockReset().
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: CHECKOUTS });
|
|
148
|
-
// Default: a healthy subscriber (no banner) — commerce-disabled builds
|
|
149
|
-
// read identically (a 404), covered explicitly below (D8).
|
|
150
|
-
mockSubscriptionMe.mockReset().mockResolvedValue({
|
|
151
|
-
ok: true,
|
|
152
|
-
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
153
|
-
});
|
|
154
|
-
mockSignOut.mockReset().mockResolvedValue(undefined);
|
|
155
|
-
mockPush.mockReset();
|
|
125
|
+
mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
|
|
126
|
+
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [CHECKOUT] });
|
|
127
|
+
mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
|
|
156
128
|
mockRefresh.mockReset();
|
|
157
|
-
mockExperimentKeys = ["ailk-founder-diagnostic", "beta-cohort"];
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("AccountPage — results + purchases", () => {
|
|
161
|
-
it("renders the signup for a configured key and skips a 404'd key", async () => {
|
|
162
|
-
await renderPage();
|
|
163
|
-
|
|
164
|
-
const results = screen.getByTestId("account-results");
|
|
165
|
-
const row = within(results).getByTestId("account-result-ailk-founder-diagnostic");
|
|
166
|
-
expect(within(row).getByText("hot")).toBeInTheDocument();
|
|
167
|
-
expect(within(row).getByText("early_bird")).toBeInTheDocument();
|
|
168
|
-
expect(within(results).queryByTestId("account-result-beta-cohort")).not.toBeInTheDocument();
|
|
169
|
-
expect(within(results).queryByText("beta-cohort")).not.toBeInTheDocument();
|
|
170
|
-
|
|
171
|
-
// One read per configured key, each forwarding the caller's cookie.
|
|
172
|
-
expect(mockSignupMe).toHaveBeenCalledTimes(2);
|
|
173
|
-
expect(mockSignupMe).toHaveBeenCalledWith("ailk-founder-diagnostic", {
|
|
174
|
-
headers: { cookie: "session=abc" },
|
|
175
|
-
});
|
|
176
|
-
expect(mockSignupMe).toHaveBeenCalledWith("beta-cohort", {
|
|
177
|
-
headers: { cookie: "session=abc" },
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("renders every checkout, newest-first order preserved, with a formatted amount", async () => {
|
|
182
|
-
await renderPage();
|
|
183
|
-
|
|
184
|
-
const purchases = screen.getByTestId("account-purchases");
|
|
185
|
-
const done = within(purchases).getByTestId("account-purchase-fc_done");
|
|
186
|
-
const open = within(purchases).getByTestId("account-purchase-fc_open");
|
|
187
|
-
expect(done).toBeInTheDocument();
|
|
188
|
-
expect(open).toBeInTheDocument();
|
|
189
|
-
expect(within(done).getByText("$49")).toBeInTheDocument();
|
|
190
|
-
expect(within(done).getByText("completed")).toBeInTheDocument();
|
|
191
|
-
expect(within(open).getByText("created")).toBeInTheDocument();
|
|
192
|
-
expect(mockCheckoutsMe).toHaveBeenCalledWith({ headers: { cookie: "session=abc" } });
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("rethrows a non-skippable signup error to the route error boundary", async () => {
|
|
196
|
-
mockSignupMe.mockResolvedValue({
|
|
197
|
-
ok: false,
|
|
198
|
-
status: 500,
|
|
199
|
-
body: { error: { code: "internal_error", message: "Boom" } },
|
|
200
|
-
});
|
|
201
|
-
await expect(Page({ params: Promise.resolve({ locale: "en" }) })).rejects.toThrow("Boom");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("treats a 403 email_unverified checkouts read as no purchases, not an error", async () => {
|
|
205
|
-
mockCheckoutsMe.mockResolvedValue({
|
|
206
|
-
ok: false,
|
|
207
|
-
status: 403,
|
|
208
|
-
body: { error: { code: "email_unverified", message: "Verify first" } },
|
|
209
|
-
});
|
|
210
|
-
await renderPage();
|
|
211
|
-
expect(within(screen.getByTestId("account-purchases")).getByText("No purchases yet")).toBeInTheDocument();
|
|
212
|
-
});
|
|
213
129
|
});
|
|
214
130
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
mockExperimentKeys = [];
|
|
218
|
-
mockCheckoutsMe.mockResolvedValue({ ok: true, body: [] });
|
|
219
|
-
await renderPage();
|
|
220
|
-
|
|
221
|
-
expect(mockSignupMe).not.toHaveBeenCalled();
|
|
222
|
-
expect(within(screen.getByTestId("account-results")).getByText("No results yet")).toBeInTheDocument();
|
|
223
|
-
expect(within(screen.getByTestId("account-purchases")).getByText("No purchases yet")).toBeInTheDocument();
|
|
224
|
-
});
|
|
131
|
+
afterAll(() => {
|
|
132
|
+
accountExperimentKeys.length = 0;
|
|
225
133
|
});
|
|
226
134
|
|
|
227
|
-
describe("
|
|
228
|
-
it("
|
|
135
|
+
describe("the Inbox — D6a tabs (AC-7)", () => {
|
|
136
|
+
it("renders Recents · Suggested · Scheduled · All with counts, Recents selected", async () => {
|
|
137
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
229
138
|
await renderPage();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
expect(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
139
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Inbox");
|
|
140
|
+
const tabs = screen.getAllByRole("tab");
|
|
141
|
+
expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
|
|
142
|
+
"inbox-tab-recents",
|
|
143
|
+
"inbox-tab-suggested",
|
|
144
|
+
"inbox-tab-scheduled",
|
|
145
|
+
"inbox-tab-all",
|
|
146
|
+
]);
|
|
147
|
+
expect(tabs.map((tab) => tab.textContent)).toEqual([
|
|
148
|
+
"Recents2",
|
|
149
|
+
"Suggested0",
|
|
150
|
+
"Scheduled0",
|
|
151
|
+
"All2",
|
|
152
|
+
]);
|
|
153
|
+
expect(screen.getByTestId("inbox-tab-recents")).toHaveAttribute("aria-selected", "true");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("result and purchase rows sit under Recents, newest first, each linking to its item page", async () => {
|
|
157
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
240
158
|
await renderPage();
|
|
241
|
-
|
|
242
|
-
|
|
159
|
+
const recents = screen.getByTestId("inbox-recents");
|
|
160
|
+
const rows = within(recents).getAllByTestId("inbox-recents-row");
|
|
161
|
+
expect(rows).toHaveLength(2);
|
|
162
|
+
// The purchase (Sep 4) is newer than the result (Sep 3).
|
|
163
|
+
expect(rows[0]).toHaveTextContent("pro");
|
|
164
|
+
expect(rows[1]).toHaveTextContent("ailk-founder-diagnostic");
|
|
165
|
+
expect(within(recents).getByText("pro").closest("a")).toHaveAttribute(
|
|
166
|
+
"href",
|
|
167
|
+
"/account/inbox/purchase/fc_done",
|
|
243
168
|
);
|
|
169
|
+
expect(
|
|
170
|
+
within(recents).getByText("ailk-founder-diagnostic").closest("a"),
|
|
171
|
+
).toHaveAttribute("href", "/account/inbox/result/ailk-founder-diagnostic");
|
|
244
172
|
});
|
|
245
|
-
});
|
|
246
173
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
expect(screen.getAllByTestId("sidebar-nav-item-profile")[0]).toHaveTextContent("Profile");
|
|
253
|
-
expect(screen.getAllByTestId("sidebar-nav-item-results")[0]).toHaveTextContent("Results");
|
|
254
|
-
expect(screen.getAllByTestId("sidebar-nav-item-purchases")[0]).toHaveTextContent("Purchases");
|
|
255
|
-
expect(screen.getAllByTestId("sidebar-nav-item-results")[0]).toHaveAttribute(
|
|
174
|
+
it("the same rows are in All; a non-default locale prefixes the item href", async () => {
|
|
175
|
+
await renderPage("fr");
|
|
176
|
+
fireEvent.mouseDown(screen.getByTestId("inbox-tab-all"));
|
|
177
|
+
const all = screen.getByTestId("inbox-all");
|
|
178
|
+
expect(within(all).getByText("pro").closest("a")).toHaveAttribute(
|
|
256
179
|
"href",
|
|
257
|
-
"/account
|
|
180
|
+
"/fr/account/inbox/purchase/fc_done",
|
|
258
181
|
);
|
|
259
182
|
});
|
|
260
|
-
});
|
|
261
183
|
|
|
262
|
-
|
|
263
|
-
|
|
184
|
+
it("with no rows, every tab still renders and Recents shows the empty line", async () => {
|
|
185
|
+
mockCheckoutsMe.mockResolvedValue({ ok: true, body: [] });
|
|
264
186
|
await renderPage();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
187
|
+
expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
|
|
188
|
+
"Recents0",
|
|
189
|
+
"Suggested0",
|
|
190
|
+
"Scheduled0",
|
|
191
|
+
"All0",
|
|
192
|
+
]);
|
|
193
|
+
expect(screen.getByTestId("inbox-empty")).toHaveTextContent("Nothing here yet.");
|
|
271
194
|
});
|
|
272
195
|
});
|
|
273
196
|
|
|
274
|
-
describe("
|
|
275
|
-
it("
|
|
197
|
+
describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
198
|
+
it("email-unverified and payment-due are For-you items in the phone stack, never Inbox rows", async () => {
|
|
199
|
+
mockGetSession.mockResolvedValue({
|
|
200
|
+
user: { ...SESSION.user, emailVerified: false },
|
|
201
|
+
});
|
|
276
202
|
mockSubscriptionMe.mockResolvedValue({
|
|
277
203
|
ok: true,
|
|
278
204
|
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
279
205
|
});
|
|
280
206
|
await renderPage();
|
|
281
|
-
|
|
207
|
+
const forYou = screen.getByTestId("inbox-for-you");
|
|
208
|
+
expect(forYou).toHaveClass("md:hidden");
|
|
209
|
+
expect(within(forYou).getByTestId("for-you-stack-count")).toHaveTextContent("2");
|
|
210
|
+
const list = screen.getByTestId("inbox-list");
|
|
211
|
+
expect(within(list).queryByText("Email not verified")).not.toBeInTheDocument();
|
|
212
|
+
expect(
|
|
213
|
+
within(list).queryByText("Your payment didn't go through"),
|
|
214
|
+
).not.toBeInTheDocument();
|
|
282
215
|
});
|
|
283
216
|
|
|
284
|
-
it("
|
|
285
|
-
mockSubscriptionMe.mockResolvedValue({
|
|
286
|
-
ok: true,
|
|
287
|
-
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
|
|
288
|
-
});
|
|
217
|
+
it("nothing for you → no For you section", async () => {
|
|
289
218
|
await renderPage();
|
|
290
|
-
expect(screen.queryByTestId("
|
|
219
|
+
expect(screen.queryByTestId("inbox-for-you")).not.toBeInTheDocument();
|
|
291
220
|
});
|
|
292
221
|
|
|
293
|
-
it("
|
|
294
|
-
|
|
295
|
-
ok: true,
|
|
296
|
-
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
|
|
297
|
-
});
|
|
222
|
+
it("a member with no name sees 'Add your name' (needs-you, Open → User settings › General)", async () => {
|
|
223
|
+
mockGetSession.mockResolvedValue({ user: { ...SESSION.user, name: "" } });
|
|
298
224
|
await renderPage();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
310
|
-
await renderPage();
|
|
311
|
-
expect(screen.getByTestId("account-page")).toBeInTheDocument();
|
|
312
|
-
expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
|
|
313
|
-
},
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
it("rejects to the route error boundary on a 500 subscription read", async () => {
|
|
317
|
-
mockSubscriptionMe.mockResolvedValue({
|
|
318
|
-
ok: false,
|
|
319
|
-
status: 500,
|
|
320
|
-
body: { error: { code: "internal_error", message: "Boom" } },
|
|
321
|
-
});
|
|
322
|
-
await expect(Page({ params: Promise.resolve({ locale: "en" }) })).rejects.toThrow("Boom");
|
|
225
|
+
const forYou = screen.getByTestId("inbox-for-you");
|
|
226
|
+
const item = within(forYou).getByTestId("for-you-item");
|
|
227
|
+
expect(item).toHaveAttribute("data-item-id", "add-name");
|
|
228
|
+
expect(item).toHaveTextContent("Add your name");
|
|
229
|
+
// DESIGN.md §5.4c — the tag is icon + label.
|
|
230
|
+
expect(within(item).getByTestId("for-you-tag").querySelector("svg")).not.toBeNull();
|
|
231
|
+
expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
|
|
232
|
+
"href",
|
|
233
|
+
"/account/settings#general",
|
|
234
|
+
);
|
|
323
235
|
});
|
|
236
|
+
});
|
|
324
237
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
329
|
-
});
|
|
238
|
+
describe("Hub unavailable (D12)", () => {
|
|
239
|
+
it("a failed read renders an error row with Retry, never an empty Inbox", async () => {
|
|
240
|
+
mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
330
241
|
await renderPage();
|
|
331
|
-
|
|
332
|
-
|
|
242
|
+
expect(screen.getByTestId("inbox-error")).toHaveTextContent(
|
|
243
|
+
"We couldn't load everything in your Inbox.",
|
|
244
|
+
);
|
|
245
|
+
expect(screen.queryByTestId("inbox-empty")).not.toBeInTheDocument();
|
|
246
|
+
fireEvent.click(screen.getByTestId("inbox-retry"));
|
|
247
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
333
248
|
});
|
|
334
249
|
|
|
335
|
-
it("
|
|
336
|
-
mockSubscriptionMe.mockResolvedValue({
|
|
337
|
-
ok: true,
|
|
338
|
-
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: false },
|
|
339
|
-
});
|
|
250
|
+
it("a failed For-you read also shows the error row", async () => {
|
|
251
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
340
252
|
await renderPage();
|
|
341
|
-
|
|
342
|
-
expect(within(banner).queryByRole("button")).not.toBeInTheDocument();
|
|
253
|
+
expect(screen.getByTestId("inbox-error")).toBeInTheDocument();
|
|
343
254
|
});
|
|
344
255
|
});
|