create-ailk 0.11.2 → 0.11.4
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 +372 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +258 -225
- 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 +169 -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 +89 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +140 -293
- 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 +148 -21
- package/templates/database/CHANGELOG.md +14 -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,30 @@
|
|
|
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
|
+
* For you tab), 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.
|
|
15
|
+
* 6. #7469 (DESIGN.md §2b as amended 2026-10-01) — the route is the
|
|
16
|
+
* phone's two main tabs. The Inbox tab is the filter tabs and the list
|
|
17
|
+
* only: no For-you stack, no bottom sheet, no large title. The For you
|
|
18
|
+
* tab lists every For-you item in one inset card, and ✕ dismisses one.
|
|
19
|
+
* At `md`+ only the Inbox panel shows (the aside holds For you).
|
|
11
20
|
*/
|
|
12
|
-
import { fireEvent, render, screen,
|
|
21
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
13
22
|
|
|
14
23
|
// The global setup mock resolves only top-level namespace keys against
|
|
15
|
-
// en.json;
|
|
16
|
-
//
|
|
17
|
-
//
|
|
24
|
+
// en.json; the area reads nested keys (`inbox.tabs.recents`, …) — a call
|
|
25
|
+
// shape real next-intl supports — so resolve dotted namespaces AND dotted
|
|
26
|
+
// keys here against the same catalog. `{name}` placeholders are filled from
|
|
27
|
+
// `values` the way next-intl does.
|
|
18
28
|
jest.mock("next-intl/server", () => {
|
|
19
29
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
20
30
|
const dig = (obj: unknown, path: string): unknown =>
|
|
@@ -27,9 +37,12 @@ jest.mock("next-intl/server", () => {
|
|
|
27
37
|
getTranslations: async (arg: string | { namespace?: string; locale?: string }) => {
|
|
28
38
|
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
29
39
|
const messages = dig(msgs, ns);
|
|
30
|
-
return (key: string): string => {
|
|
40
|
+
return (key: string, values?: Record<string, unknown>): string => {
|
|
31
41
|
const value = dig(messages, key);
|
|
32
|
-
|
|
42
|
+
if (typeof value !== "string") return `${ns}.${key}`;
|
|
43
|
+
return value.replace(/\{(\w+)\}/g, (m, name: string) =>
|
|
44
|
+
values && name in values ? String(values[name]) : m,
|
|
45
|
+
);
|
|
33
46
|
};
|
|
34
47
|
},
|
|
35
48
|
};
|
|
@@ -42,7 +55,7 @@ jest.mock("next/headers", () => ({
|
|
|
42
55
|
}));
|
|
43
56
|
|
|
44
57
|
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
|
|
58
|
+
// 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
59
|
jest.mock("../../../../../auth", () => ({
|
|
47
60
|
__esModule: true,
|
|
48
61
|
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
@@ -51,44 +64,33 @@ jest.mock("../../../../../auth", () => ({
|
|
|
51
64
|
const mockSignupMe = jest.fn();
|
|
52
65
|
const mockCheckoutsMe = jest.fn();
|
|
53
66
|
const mockSubscriptionMe = jest.fn();
|
|
54
|
-
const mockSubscriptionPortalSession = jest.fn();
|
|
55
67
|
jest.mock("@working-theory/api-client", () => ({
|
|
56
68
|
__esModule: true,
|
|
57
69
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
58
70
|
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
|
-
},
|
|
71
|
+
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
72
72
|
}));
|
|
73
73
|
|
|
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
74
|
const mockRefresh = jest.fn();
|
|
82
75
|
jest.mock("next/navigation", () => ({
|
|
83
76
|
__esModule: true,
|
|
84
|
-
useRouter: () => ({ push:
|
|
85
|
-
usePathname: () => "/
|
|
77
|
+
useRouter: () => ({ push: jest.fn(), refresh: mockRefresh }),
|
|
78
|
+
usePathname: () => "/account",
|
|
86
79
|
}));
|
|
87
80
|
|
|
81
|
+
import { PhoneTabProvider } from "@working-theory/templates/account";
|
|
82
|
+
|
|
83
|
+
import { accountExperimentKeys } from "../../../../../lib/account-config";
|
|
88
84
|
import Page from "../page";
|
|
89
85
|
|
|
90
86
|
const SESSION = {
|
|
91
|
-
user: {
|
|
87
|
+
user: {
|
|
88
|
+
id: "user_1",
|
|
89
|
+
name: "Ada",
|
|
90
|
+
email: "ada@example.com",
|
|
91
|
+
emailVerified: true,
|
|
92
|
+
createdAt: new Date("2026-09-01T00:00:00.000Z"),
|
|
93
|
+
},
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
const SIGNUP = {
|
|
@@ -101,244 +103,275 @@ const SIGNUP = {
|
|
|
101
103
|
answers: { q3: null },
|
|
102
104
|
};
|
|
103
105
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const CHECKOUT = {
|
|
107
|
+
id: "fc_done",
|
|
108
|
+
flowSlug: "founder-flow",
|
|
109
|
+
experimentKey: "ailk-founder-diagnostic",
|
|
110
|
+
offerKey: "pro",
|
|
111
|
+
status: "completed",
|
|
112
|
+
amountMinor: 4900,
|
|
113
|
+
currency: "usd",
|
|
114
|
+
period: "monthly",
|
|
115
|
+
completedAt: "2026-09-04T10:00:00.000Z",
|
|
116
|
+
createdAt: "2026-09-04T09:58:00.000Z",
|
|
108
117
|
};
|
|
109
118
|
|
|
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
|
-
];
|
|
119
|
+
const NO_SUBSCRIPTION = {
|
|
120
|
+
ok: true,
|
|
121
|
+
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
122
|
+
};
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
/** Renders the page on a phone tab — the frame's PhoneTabProvider. */
|
|
125
|
+
async function renderPage(locale = "en", tab = "inbox"): Promise<void> {
|
|
126
|
+
const element = await Page({ params: Promise.resolve({ locale }) });
|
|
127
|
+
render(<PhoneTabProvider tab={tab}>{element}</PhoneTabProvider>);
|
|
140
128
|
}
|
|
141
129
|
|
|
142
|
-
|
|
143
|
-
mockGetSession.
|
|
144
|
-
|
|
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({
|
|
130
|
+
const UNVERIFIED_AND_PAST_DUE = (): void => {
|
|
131
|
+
mockGetSession.mockResolvedValue({
|
|
132
|
+
user: { ...SESSION.user, emailVerified: false },
|
|
133
|
+
});
|
|
134
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
151
135
|
ok: true,
|
|
152
|
-
body: { actionRequired:
|
|
136
|
+
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
153
137
|
});
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
window.localStorage.clear();
|
|
142
|
+
accountExperimentKeys.length = 0;
|
|
143
|
+
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
144
|
+
mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
|
|
145
|
+
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [CHECKOUT] });
|
|
146
|
+
mockSubscriptionMe.mockReset().mockResolvedValue(NO_SUBSCRIPTION);
|
|
156
147
|
mockRefresh.mockReset();
|
|
157
|
-
mockExperimentKeys = ["ailk-founder-diagnostic", "beta-cohort"];
|
|
158
148
|
});
|
|
159
149
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
afterAll(() => {
|
|
151
|
+
accountExperimentKeys.length = 0;
|
|
152
|
+
});
|
|
163
153
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
expect(
|
|
177
|
-
|
|
178
|
-
|
|
154
|
+
describe("the Inbox — D6a tabs (AC-7)", () => {
|
|
155
|
+
it("renders Recents · Suggested · Scheduled · All with counts, Recents selected", async () => {
|
|
156
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
157
|
+
await renderPage();
|
|
158
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Inbox");
|
|
159
|
+
const tabs = screen.getAllByRole("tab");
|
|
160
|
+
expect(tabs.map((tab) => tab.getAttribute("data-testid"))).toEqual([
|
|
161
|
+
"inbox-tab-recents",
|
|
162
|
+
"inbox-tab-suggested",
|
|
163
|
+
"inbox-tab-scheduled",
|
|
164
|
+
"inbox-tab-all",
|
|
165
|
+
]);
|
|
166
|
+
expect(tabs.map((tab) => tab.textContent)).toEqual([
|
|
167
|
+
"Recents2",
|
|
168
|
+
"Suggested0",
|
|
169
|
+
"Scheduled0",
|
|
170
|
+
"All2",
|
|
171
|
+
]);
|
|
172
|
+
expect(screen.getByTestId("inbox-tab-recents")).toHaveAttribute("aria-selected", "true");
|
|
179
173
|
});
|
|
180
174
|
|
|
181
|
-
it("
|
|
175
|
+
it("result and purchase rows sit under Recents, newest first, each linking to its item page", async () => {
|
|
176
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
182
177
|
await renderPage();
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
expect(
|
|
188
|
-
expect(
|
|
189
|
-
expect(within(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
const recents = screen.getByTestId("inbox-recents");
|
|
179
|
+
const rows = within(recents).getAllByTestId("inbox-recents-row");
|
|
180
|
+
expect(rows).toHaveLength(2);
|
|
181
|
+
// The purchase (Sep 4) is newer than the result (Sep 3).
|
|
182
|
+
expect(rows[0]).toHaveTextContent("pro");
|
|
183
|
+
expect(rows[1]).toHaveTextContent("ailk-founder-diagnostic");
|
|
184
|
+
expect(within(recents).getByText("pro").closest("a")).toHaveAttribute(
|
|
185
|
+
"href",
|
|
186
|
+
"/account/inbox/purchase/fc_done",
|
|
187
|
+
);
|
|
188
|
+
expect(
|
|
189
|
+
within(recents).getByText("ailk-founder-diagnostic").closest("a"),
|
|
190
|
+
).toHaveAttribute("href", "/account/inbox/result/ailk-founder-diagnostic");
|
|
193
191
|
});
|
|
194
192
|
|
|
195
|
-
it("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
it("the same rows are in All; a non-default locale prefixes the item href", async () => {
|
|
194
|
+
await renderPage("fr");
|
|
195
|
+
fireEvent.mouseDown(screen.getByTestId("inbox-tab-all"));
|
|
196
|
+
const all = screen.getByTestId("inbox-all");
|
|
197
|
+
expect(within(all).getByText("pro").closest("a")).toHaveAttribute(
|
|
198
|
+
"href",
|
|
199
|
+
"/fr/account/inbox/purchase/fc_done",
|
|
200
|
+
);
|
|
202
201
|
});
|
|
203
202
|
|
|
204
|
-
it("
|
|
205
|
-
mockCheckoutsMe.mockResolvedValue({
|
|
206
|
-
ok: false,
|
|
207
|
-
status: 403,
|
|
208
|
-
body: { error: { code: "email_unverified", message: "Verify first" } },
|
|
209
|
-
});
|
|
203
|
+
it("with no rows, every tab still renders and Recents shows the empty line", async () => {
|
|
204
|
+
mockCheckoutsMe.mockResolvedValue({ ok: true, body: [] });
|
|
210
205
|
await renderPage();
|
|
211
|
-
expect(
|
|
206
|
+
expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
|
|
207
|
+
"Recents0",
|
|
208
|
+
"Suggested0",
|
|
209
|
+
"Scheduled0",
|
|
210
|
+
"All0",
|
|
211
|
+
]);
|
|
212
|
+
expect(screen.getByTestId("inbox-empty")).toHaveTextContent("Nothing here yet.");
|
|
212
213
|
});
|
|
213
214
|
});
|
|
214
215
|
|
|
215
|
-
describe("
|
|
216
|
-
it("
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
217
|
+
it("email-unverified and payment-due are For you tab items, never Inbox rows", async () => {
|
|
218
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
219
|
+
await renderPage("en", "for-you");
|
|
220
|
+
const forYou = screen.getByTestId("inbox-panel-for-you");
|
|
221
|
+
expect(within(forYou).getAllByTestId("for-you-item")).toHaveLength(2);
|
|
222
|
+
const list = screen.getByTestId("inbox-list");
|
|
223
|
+
expect(within(list).queryByText("Email not verified")).not.toBeInTheDocument();
|
|
224
|
+
expect(
|
|
225
|
+
within(list).queryByText("Your payment didn't go through"),
|
|
226
|
+
).not.toBeInTheDocument();
|
|
227
|
+
});
|
|
220
228
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
it("a member with no name sees 'Add your name' (needs-you, Open → User settings › General)", async () => {
|
|
230
|
+
mockGetSession.mockResolvedValue({ user: { ...SESSION.user, name: "" } });
|
|
231
|
+
await renderPage("en", "for-you");
|
|
232
|
+
const forYou = screen.getByTestId("inbox-panel-for-you");
|
|
233
|
+
const item = within(forYou).getByTestId("for-you-item");
|
|
234
|
+
expect(item).toHaveAttribute("data-item-id", "add-name");
|
|
235
|
+
expect(item).toHaveTextContent("Add your name");
|
|
236
|
+
// DESIGN.md §5.4c — the tag is icon + label.
|
|
237
|
+
expect(within(item).getByTestId("for-you-tag").querySelector("svg")).not.toBeNull();
|
|
238
|
+
expect(within(item).getByTestId("for-you-open")).toHaveAttribute(
|
|
239
|
+
"href",
|
|
240
|
+
"/account/settings#general",
|
|
241
|
+
);
|
|
224
242
|
});
|
|
225
243
|
});
|
|
226
244
|
|
|
227
|
-
describe("
|
|
228
|
-
it("
|
|
245
|
+
describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
246
|
+
it("holds the filter tabs and the list only — no For-you stack and no bottom sheet", async () => {
|
|
247
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
229
248
|
await renderPage();
|
|
230
|
-
const
|
|
231
|
-
expect(within(
|
|
232
|
-
expect(
|
|
233
|
-
expect(
|
|
249
|
+
const inbox = screen.getByTestId("inbox-panel-inbox");
|
|
250
|
+
expect(within(inbox).getByTestId("inbox-list")).toBeInTheDocument();
|
|
251
|
+
expect(screen.queryByTestId("for-you-stack")).not.toBeInTheDocument();
|
|
252
|
+
expect(screen.queryByTestId("for-you-stack-count")).not.toBeInTheDocument();
|
|
253
|
+
expect(screen.queryByTestId("for-you-sheet")).not.toBeInTheDocument();
|
|
254
|
+
expect(screen.queryByTestId("inbox-for-you")).not.toBeInTheDocument();
|
|
234
255
|
});
|
|
235
256
|
|
|
236
|
-
it("shows the
|
|
237
|
-
|
|
238
|
-
user: { ...SESSION.user, emailVerified: false },
|
|
239
|
-
});
|
|
257
|
+
it("shows on the phone and at md+, while the For you panel stays hidden", async () => {
|
|
258
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
240
259
|
await renderPage();
|
|
241
|
-
expect(
|
|
242
|
-
|
|
243
|
-
);
|
|
260
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("hidden");
|
|
261
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("md:hidden");
|
|
262
|
+
expect(screen.getByTestId("inbox-panel-for-you")).toHaveClass("hidden");
|
|
244
263
|
});
|
|
245
|
-
});
|
|
246
264
|
|
|
247
|
-
|
|
248
|
-
it("renders the three in-page anchors with their labels", async () => {
|
|
265
|
+
it("the phone has no large title or lead — both stay at md+", async () => {
|
|
249
266
|
await renderPage();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
expect(screen.
|
|
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(
|
|
256
|
-
"href",
|
|
257
|
-
"/account#results",
|
|
258
|
-
);
|
|
267
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveClass("max-md:sr-only");
|
|
268
|
+
expect(screen.getByRole("heading", { level: 1 })).not.toHaveClass("hidden");
|
|
269
|
+
expect(screen.getByText("What changed, and what's coming.")).toHaveClass("hidden", "md:block");
|
|
259
270
|
});
|
|
260
|
-
});
|
|
261
271
|
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
it("the list sits in a bordered card, inset by the canvas on every side", async () => {
|
|
273
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
264
274
|
await renderPage();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
expect(
|
|
269
|
-
expect(
|
|
270
|
-
expect(mockSignOut.mock.invocationCallOrder[0]).toBeLessThan(mockPush.mock.invocationCallOrder[0]!);
|
|
275
|
+
// The shell's main slot insets the canvas (p-4 below md); the page adds
|
|
276
|
+
// no full-bleed escape, and the list is RowGroup's bordered box.
|
|
277
|
+
const page = screen.getByTestId("account-inbox");
|
|
278
|
+
expect(page.className).not.toMatch(/-mx-|w-screen/);
|
|
279
|
+
expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(/-mx-|w-screen/);
|
|
271
280
|
});
|
|
272
281
|
});
|
|
273
282
|
|
|
274
|
-
describe("
|
|
275
|
-
it("
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
284
|
+
it("lists every item with its type tag, title, Open or View, and ✕", async () => {
|
|
285
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
286
|
+
mockGetSession.mockResolvedValue({
|
|
287
|
+
user: { ...SESSION.user, name: "", emailVerified: false },
|
|
279
288
|
});
|
|
280
|
-
await renderPage();
|
|
281
|
-
|
|
289
|
+
await renderPage("en", "for-you");
|
|
290
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
291
|
+
// Phone-only: shown below md, hidden at md+ where the aside holds it.
|
|
292
|
+
expect(panel).toHaveClass("md:hidden");
|
|
293
|
+
expect(panel).not.toHaveClass("hidden");
|
|
294
|
+
// The Inbox panel is hidden on the phone but still shows at md+.
|
|
295
|
+
expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass("hidden", "md:block");
|
|
296
|
+
const card = within(panel).getByTestId("for-you-list-card");
|
|
297
|
+
const items = within(card).getAllByTestId("for-you-item");
|
|
298
|
+
expect(items).toHaveLength(3);
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
expect(within(item).getByTestId("for-you-tag")).toBeInTheDocument();
|
|
301
|
+
expect(
|
|
302
|
+
within(item).queryByTestId("for-you-open") ?? within(item).queryByTestId("for-you-view"),
|
|
303
|
+
).not.toBeNull();
|
|
304
|
+
expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(/^Dismiss/);
|
|
305
|
+
}
|
|
306
|
+
expect(within(panel).getByText("Add your name")).toBeInTheDocument();
|
|
307
|
+
expect(within(panel).getByText("Email not verified")).toBeInTheDocument();
|
|
308
|
+
expect(within(panel).getByText("Your payment didn't go through")).toBeInTheDocument();
|
|
282
309
|
});
|
|
283
310
|
|
|
284
|
-
it("
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
it("✕ dismisses an item — it leaves the list", async () => {
|
|
312
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
313
|
+
await renderPage("en", "for-you");
|
|
314
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
315
|
+
const [first] = within(panel).getAllByTestId("for-you-item");
|
|
316
|
+
const id = first!.getAttribute("data-item-id");
|
|
317
|
+
fireEvent.click(within(first!).getByTestId("for-you-dismiss"));
|
|
318
|
+
const left = within(panel).getAllByTestId("for-you-item");
|
|
319
|
+
expect(left).toHaveLength(1);
|
|
320
|
+
expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(id);
|
|
291
321
|
});
|
|
292
322
|
|
|
293
|
-
it("
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
await renderPage();
|
|
299
|
-
expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
|
|
323
|
+
it("the For you tab keeps the page's h1 (visually hidden on the phone)", async () => {
|
|
324
|
+
await renderPage("en", "for-you");
|
|
325
|
+
const h1 = screen.getByRole("heading", { level: 1 });
|
|
326
|
+
expect(h1).toHaveTextContent("Inbox");
|
|
327
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toContainElement(h1);
|
|
300
328
|
});
|
|
301
329
|
|
|
302
|
-
it
|
|
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");
|
|
330
|
+
it("with nothing waiting the tab shows its empty line, not a blank page", async () => {
|
|
331
|
+
await renderPage("en", "for-you");
|
|
332
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
333
|
+
expect(within(panel).queryByTestId("for-you-item")).not.toBeInTheDocument();
|
|
334
|
+
expect(within(panel).getByTestId("for-you-empty")).toHaveTextContent(
|
|
335
|
+
"Nothing here right now.",
|
|
336
|
+
);
|
|
323
337
|
});
|
|
338
|
+
});
|
|
324
339
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
329
|
-
});
|
|
340
|
+
describe("Hub unavailable (D12)", () => {
|
|
341
|
+
it("a failed read renders an error row with Retry, never an empty Inbox", async () => {
|
|
342
|
+
mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
330
343
|
await renderPage();
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
expect(screen.getByTestId("inbox-error")).toHaveTextContent(
|
|
345
|
+
"We couldn't load everything in your Inbox.",
|
|
346
|
+
);
|
|
347
|
+
expect(screen.queryByTestId("inbox-empty")).not.toBeInTheDocument();
|
|
348
|
+
fireEvent.click(screen.getByTestId("inbox-retry"));
|
|
349
|
+
expect(mockRefresh).toHaveBeenCalledTimes(1);
|
|
333
350
|
});
|
|
334
351
|
|
|
335
|
-
it("
|
|
336
|
-
mockSubscriptionMe.mockResolvedValue({
|
|
337
|
-
ok: true,
|
|
338
|
-
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: false },
|
|
339
|
-
});
|
|
352
|
+
it("a failed For-you read also shows the error row", async () => {
|
|
353
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
340
354
|
await renderPage();
|
|
341
|
-
|
|
342
|
-
|
|
355
|
+
expect(
|
|
356
|
+
within(screen.getByTestId("inbox-panel-inbox")).getByTestId("inbox-error"),
|
|
357
|
+
).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("on the phone For you tab, a failed For-you read shows the error row above the list (#7469)", async () => {
|
|
361
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
362
|
+
await renderPage("en", "for-you");
|
|
363
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
364
|
+
expect(within(panel).getByTestId("inbox-error")).toHaveTextContent(
|
|
365
|
+
"We couldn't load everything in your Inbox.",
|
|
366
|
+
);
|
|
367
|
+
// Never a silently empty list (D12): the empty line gives way to the error row.
|
|
368
|
+
expect(within(panel).queryByTestId("for-you-empty")).not.toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
|
|
372
|
+
mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
373
|
+
await renderPage("en", "for-you");
|
|
374
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
375
|
+
expect(within(panel).queryByTestId("inbox-error")).not.toBeInTheDocument();
|
|
343
376
|
});
|
|
344
377
|
});
|