@remit/web-client 0.0.300 → 0.0.301

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.300",
3
+ "version": "0.0.301",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -77,6 +77,7 @@
77
77
  "@remit/api-http-client": "*",
78
78
  "@remit/data-ports": "*",
79
79
  "@remit/domain-enums": "*",
80
+ "@remit/mail-oauth-service": "*",
80
81
  "@remit/ui": "*",
81
82
  "@tanstack/react-query": "^5",
82
83
  "@tanstack/react-query-devtools": "^5",
@@ -24,11 +24,10 @@ const readCognitoEnv = (): CognitoEnv | null => {
24
24
  };
25
25
  };
26
26
 
27
- const cognitoEnv = readCognitoEnv();
28
-
29
- export const isCognitoConfigured = (): boolean => cognitoEnv !== null;
27
+ export const isCognitoConfigured = (): boolean => readCognitoEnv() !== null;
30
28
 
31
29
  export const configureAmplify = (): void => {
30
+ const cognitoEnv = readCognitoEnv();
32
31
  if (!cognitoEnv) {
33
32
  console.warn(
34
33
  "[auth] Cognito user pool is not configured. Running in local dev mode without Cognito auth.",
@@ -0,0 +1,90 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+ import { cognitoAuthProvider } from "@/auth/cognito-provider";
4
+ import { AppStory } from "@/mocks/story-frame/AppStory";
5
+ import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
6
+ import { mailWorld } from "@/mocks/story-frame/mail-world";
7
+ import { withRuntimeConfig } from "@/mocks/story-frame/session";
8
+
9
+ const userPool = withRuntimeConfig({
10
+ cognito: {
11
+ userPoolId: "eu-west-1_Story0001",
12
+ clientId: "story-client-id",
13
+ region: "eu-west-1",
14
+ },
15
+ });
16
+
17
+ const meta = {
18
+ title: "Playground/Shipped/Sign in/Cognito",
19
+ component: AppStory,
20
+ args: { url: "/mail/brief", authProvider: cognitoAuthProvider },
21
+ parameters: {
22
+ layout: "fullscreen",
23
+ theme: "dark",
24
+ msw: { handlers: mailHandlers(mailWorld()) },
25
+ },
26
+ beforeEach: userPool,
27
+ } satisfies Meta<typeof AppStory>;
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ const phone = { viewport: { value: "mobile", isRotated: false } };
33
+
34
+ const signInForm = async (canvasElement: HTMLElement) => {
35
+ const canvas = within(canvasElement);
36
+ await expect(
37
+ await canvas.findByRole("tab", { name: "Sign In", selected: true }),
38
+ ).toBeVisible();
39
+ await expect(canvas.getByLabelText("Username")).toHaveAttribute(
40
+ "placeholder",
41
+ "Enter your username",
42
+ );
43
+ await expect(canvas.getByLabelText("Password")).toHaveAttribute(
44
+ "placeholder",
45
+ "Enter your password",
46
+ );
47
+ await expect(canvas.getByRole("button", { name: "Sign in" })).toBeVisible();
48
+ await expect(canvas.queryByText("Q3 planning")).toBeNull();
49
+ };
50
+
51
+ export const SignInMobile: Story = {
52
+ name: "Sign In — mobile",
53
+ globals: phone,
54
+ play: async ({ canvasElement }) => signInForm(canvasElement),
55
+ };
56
+
57
+ export const CreateAccountMobile: Story = {
58
+ name: "Create Account — mobile",
59
+ globals: phone,
60
+ play: async ({ canvasElement }) => {
61
+ const canvas = within(canvasElement);
62
+ await userEvent.click(
63
+ await canvas.findByRole("tab", { name: "Create Account" }),
64
+ );
65
+ await expect(
66
+ await canvas.findByPlaceholderText("Confirm your password"),
67
+ ).toBeVisible();
68
+ },
69
+ };
70
+
71
+ export const SignInDesktop: Story = {
72
+ name: "Sign In — desktop",
73
+ play: async ({ canvasElement }) => signInForm(canvasElement),
74
+ };
75
+
76
+ export const SignInLight: Story = {
77
+ name: "Sign In — light",
78
+ parameters: { theme: "light" },
79
+ play: async ({ canvasElement }) => signInForm(canvasElement),
80
+ };
81
+
82
+ export const LocalDevBanner: Story = {
83
+ name: "Local dev banner",
84
+ beforeEach: withRuntimeConfig({}),
85
+ play: async ({ canvasElement }) => {
86
+ const canvas = within(canvasElement);
87
+ await expect(await canvas.findByText("Local dev")).toBeVisible();
88
+ await expect(await canvas.findByText("Q3 planning")).toBeVisible();
89
+ },
90
+ };
@@ -0,0 +1,128 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { HttpResponse, http } from "msw";
3
+ import { expect, userEvent, within } from "storybook/test";
4
+ import { betterAuthProvider } from "@/auth/better-auth-provider";
5
+ import { AppStory } from "@/mocks/story-frame/AppStory";
6
+ import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
7
+ import { mailWorld } from "@/mocks/story-frame/mail-world";
8
+ import { withRuntimeConfig } from "@/mocks/story-frame/session";
9
+
10
+ const signedOut = http.get("/api/auth/get-session", () =>
11
+ HttpResponse.json(null),
12
+ );
13
+
14
+ const meta = {
15
+ title: "Playground/Shipped/Sign in/Email and password",
16
+ component: AppStory,
17
+ args: { url: "/mail/brief", authProvider: betterAuthProvider },
18
+ parameters: {
19
+ layout: "fullscreen",
20
+ msw: { handlers: [signedOut, ...mailHandlers(mailWorld())] },
21
+ },
22
+ beforeEach: withRuntimeConfig({ betterAuthEnabled: true }),
23
+ } satisfies Meta<typeof AppStory>;
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ const submit = (canvasElement: HTMLElement): HTMLElement => {
29
+ const button = canvasElement.querySelector<HTMLElement>(
30
+ 'button[type="submit"]',
31
+ );
32
+ if (!button) throw new Error("the sign-in form has no submit button");
33
+ return button;
34
+ };
35
+
36
+ const fill = async (canvasElement: HTMLElement) => {
37
+ const canvas = within(canvasElement);
38
+ await userEvent.type(
39
+ await canvas.findByLabelText("Email"),
40
+ "alice@example.com",
41
+ );
42
+ await userEvent.type(canvas.getByLabelText("Password"), "not-my-password");
43
+ };
44
+
45
+ export const SignIn: Story = {
46
+ play: async ({ canvasElement }) => {
47
+ const canvas = within(canvasElement);
48
+ await expect(
49
+ await canvas.findByRole("button", { name: "Sign in", pressed: true }),
50
+ ).toBeVisible();
51
+ await expect(canvas.getByLabelText("Email")).toBeVisible();
52
+ await expect(canvas.queryByLabelText("Name")).toBeNull();
53
+ await expect(canvas.queryByText("Q3 planning")).toBeNull();
54
+ },
55
+ };
56
+
57
+ export const CreateAccount: Story = {
58
+ play: async ({ canvasElement }) => {
59
+ const canvas = within(canvasElement);
60
+ await userEvent.click(
61
+ await canvas.findByRole("button", { name: "Create account" }),
62
+ );
63
+ await expect(await canvas.findByLabelText("Name")).toBeVisible();
64
+ await expect(
65
+ canvas.getByRole("button", { name: "Create account", pressed: true }),
66
+ ).toBeVisible();
67
+ },
68
+ };
69
+
70
+ export const WrongPassword: Story = {
71
+ parameters: {
72
+ msw: {
73
+ handlers: [
74
+ signedOut,
75
+ http.post("/api/auth/sign-in/email", () =>
76
+ HttpResponse.json(
77
+ {
78
+ code: "INVALID_EMAIL_OR_PASSWORD",
79
+ message: "Invalid email or password",
80
+ },
81
+ { status: 401 },
82
+ ),
83
+ ),
84
+ ...mailHandlers(mailWorld()),
85
+ ],
86
+ },
87
+ },
88
+ play: async ({ canvasElement }) => {
89
+ const canvas = within(canvasElement);
90
+ await fill(canvasElement);
91
+ await userEvent.click(submit(canvasElement));
92
+ await expect(
93
+ await canvas.findByText("Invalid email or password"),
94
+ ).toBeVisible();
95
+ },
96
+ };
97
+
98
+ export const SignupsClosed: Story = {
99
+ parameters: {
100
+ msw: {
101
+ handlers: [
102
+ signedOut,
103
+ http.post("/api/auth/sign-up/email", () =>
104
+ HttpResponse.json(
105
+ {
106
+ code: "EMAIL_PASSWORD_SIGN_UP_DISABLED",
107
+ message: "Email and password sign up is not enabled",
108
+ },
109
+ { status: 400 },
110
+ ),
111
+ ),
112
+ ...mailHandlers(mailWorld()),
113
+ ],
114
+ },
115
+ },
116
+ play: async ({ canvasElement }) => {
117
+ const canvas = within(canvasElement);
118
+ await userEvent.click(
119
+ await canvas.findByRole("button", { name: "Create account" }),
120
+ );
121
+ await userEvent.type(await canvas.findByLabelText("Name"), "Alice");
122
+ await fill(canvasElement);
123
+ await userEvent.click(submit(canvasElement));
124
+ await expect(
125
+ await canvas.findByText(/New signups are currently closed/),
126
+ ).toBeVisible();
127
+ },
128
+ };
@@ -0,0 +1,460 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, waitFor, within } from "storybook/test";
3
+ import {
4
+ REDIRECT_STALL_MESSAGE,
5
+ REDIRECT_STALL_MS,
6
+ } from "@/hooks/useRedirectEnded";
7
+ import { AppStory } from "@/mocks/story-frame/AppStory";
8
+ import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
9
+ import { mailWorld } from "@/mocks/story-frame/mail-world";
10
+ import {
11
+ NEW_ACCOUNT_ID,
12
+ type OnboardingHandlerOptions,
13
+ onboardingHandlers,
14
+ } from "@/mocks/story-frame/onboarding-handlers";
15
+ import { makeAccount } from "@/test-support/fixtures";
16
+
17
+ const EMAIL = "alice@northwind.example";
18
+
19
+ const firstRun = mailWorld({ accounts: [], threads: [], outbox: [] });
20
+
21
+ const handlers = (
22
+ options: OnboardingHandlerOptions = {},
23
+ world = firstRun,
24
+ ) => ({
25
+ msw: { handlers: [...onboardingHandlers(options), ...mailHandlers(world)] },
26
+ });
27
+
28
+ const meta = {
29
+ title: "Playground/Shipped/Onboarding/Wizard",
30
+ component: AppStory,
31
+ args: { url: "/onboarding" },
32
+ parameters: { layout: "fullscreen", ...handlers() },
33
+ } satisfies Meta<typeof AppStory>;
34
+ export default meta;
35
+
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ type Canvas = ReturnType<typeof within>;
39
+
40
+ const phone = { viewport: { value: "mobile", isRotated: false } };
41
+
42
+ const press = async (canvas: Canvas, name: string | RegExp) =>
43
+ userEvent.click(await canvas.findByRole("button", { name }));
44
+
45
+ const toConnector = (canvas: Canvas) => press(canvas, "Add your first account");
46
+
47
+ const toAddress = async (canvas: Canvas) => {
48
+ await toConnector(canvas);
49
+ await press(canvas, "Continue with IMAP");
50
+ };
51
+
52
+ const toServers = async (canvas: Canvas, email = EMAIL) => {
53
+ await toAddress(canvas);
54
+ await userEvent.type(await canvas.findByLabelText("Email address"), email);
55
+ await press(canvas, "Continue");
56
+ await canvas.findByText("Confirm server settings");
57
+ };
58
+
59
+ const toCredentials = async (canvas: Canvas) => {
60
+ await toServers(canvas);
61
+ await press(canvas, "Continue");
62
+ await canvas.findByText("Sign in to northwind.example");
63
+ };
64
+
65
+ const toTest = async (canvas: Canvas) => {
66
+ await toCredentials(canvas);
67
+ await userEvent.type(canvas.getByLabelText(/Password/), "app-password");
68
+ await press(canvas, "Test connection");
69
+ };
70
+
71
+ const toMicrosoft = async (canvas: Canvas) => {
72
+ await toConnector(canvas);
73
+ await userEvent.click(await canvas.findByText("Outlook / Microsoft 365"));
74
+ await press(canvas, "Continue with Microsoft");
75
+ return canvas.findByRole("button", { name: "Sign in with Microsoft" });
76
+ };
77
+
78
+ export const Welcome: Story = {
79
+ play: async ({ canvasElement }) => {
80
+ const canvas = within(canvasElement);
81
+ await expect(await canvas.findByText("Welcome to Remit")).toBeVisible();
82
+ await expect(
83
+ canvas.getByRole("button", { name: "Import a config file" }),
84
+ ).toBeVisible();
85
+ },
86
+ };
87
+
88
+ export const Walkthrough: Story = {
89
+ play: async ({ canvasElement }) => {
90
+ const canvas = within(canvasElement);
91
+ await toTest(canvas);
92
+ await expect(
93
+ await canvas.findByText(`Syncing ${EMAIL}`, undefined, {
94
+ timeout: 4000,
95
+ }),
96
+ ).toBeVisible();
97
+ await expect(
98
+ await canvas.findByRole("button", { name: "Go to inbox" }),
99
+ ).toBeEnabled();
100
+ },
101
+ };
102
+
103
+ export const ConnectorPicker: Story = {
104
+ play: async ({ canvasElement }) => {
105
+ const canvas = within(canvasElement);
106
+ await toConnector(canvas);
107
+ await expect(
108
+ await canvas.findByText("How does this account connect?"),
109
+ ).toBeVisible();
110
+ await expect(
111
+ canvas.getByRole("button", { name: "Continue with IMAP" }),
112
+ ).toBeVisible();
113
+ await expect(canvas.queryByText(/Google/)).toBeNull();
114
+ },
115
+ };
116
+
117
+ export const ConnectorPickerMicrosoft: Story = {
118
+ play: async ({ canvasElement }) => {
119
+ const canvas = within(canvasElement);
120
+ await toConnector(canvas);
121
+ await userEvent.click(await canvas.findByText("Outlook / Microsoft 365"));
122
+ await expect(
123
+ canvas.getByRole("button", { name: "Continue with Microsoft" }),
124
+ ).toBeVisible();
125
+ },
126
+ };
127
+
128
+ export const ConnectorPickerInSettings: Story = {
129
+ args: { url: "/settings/accounts" },
130
+ parameters: handlers({}, mailWorld()),
131
+ play: async ({ canvasElement }) => {
132
+ const canvas = within(canvasElement);
133
+ await press(canvas, "Add account");
134
+ await expect(
135
+ await canvas.findByText("How does this account connect?"),
136
+ ).toBeVisible();
137
+ await expect(canvas.queryByRole("button", { name: "Back" })).toBeNull();
138
+ },
139
+ };
140
+
141
+ export const ConnectorPickerPhone: Story = {
142
+ globals: phone,
143
+ play: async ({ canvasElement }) => {
144
+ const canvas = within(canvasElement);
145
+ await toConnector(canvas);
146
+ await expect(
147
+ await canvas.findByRole("button", { name: "Continue with IMAP" }),
148
+ ).toBeVisible();
149
+ },
150
+ };
151
+
152
+ export const ConnectorPickerMicrosoftPhone: Story = {
153
+ globals: phone,
154
+ play: async ({ canvasElement }) => {
155
+ const canvas = within(canvasElement);
156
+ await toConnector(canvas);
157
+ await userEvent.click(await canvas.findByText("Outlook / Microsoft 365"));
158
+ await expect(
159
+ canvas.getByRole("button", { name: "Continue with Microsoft" }),
160
+ ).toBeVisible();
161
+ },
162
+ };
163
+
164
+ export const MicrosoftEmail: Story = {
165
+ play: async ({ canvasElement }) => {
166
+ const canvas = within(canvasElement);
167
+ await expect(await toMicrosoft(canvas)).toBeEnabled();
168
+ await expect(
169
+ canvas.getByLabelText("Email address (optional)"),
170
+ ).toBeVisible();
171
+ await expect(canvas.getByRole("button", { name: "Back" })).toBeVisible();
172
+ },
173
+ };
174
+
175
+ export const MicrosoftEmailInSettings: Story = {
176
+ args: { url: "/settings/accounts" },
177
+ parameters: handlers({}, mailWorld()),
178
+ play: async ({ canvasElement }) => {
179
+ const canvas = within(canvasElement);
180
+ await press(canvas, "Add account");
181
+ await userEvent.click(await canvas.findByText("Outlook / Microsoft 365"));
182
+ await press(canvas, "Continue with Microsoft");
183
+ await expect(
184
+ await canvas.findByRole("button", { name: "Sign in with Microsoft" }),
185
+ ).toBeVisible();
186
+ await expect(canvas.getByRole("button", { name: "Back" })).toBeVisible();
187
+ },
188
+ };
189
+
190
+ const realSetTimeout = window.setTimeout;
191
+
192
+ const stallAtOnce: Window["setTimeout"] = (handler, delay, ...rest) =>
193
+ realSetTimeout(handler, delay === REDIRECT_STALL_MS ? 0 : delay, ...rest);
194
+
195
+ export const MicrosoftRedirectStalled: Story = {
196
+ beforeEach: () => () => {
197
+ Reflect.set(window, "setTimeout", realSetTimeout);
198
+ },
199
+ play: async ({ canvasElement }) => {
200
+ const canvas = within(canvasElement);
201
+ const signIn = await toMicrosoft(canvas);
202
+ Reflect.set(window, "setTimeout", stallAtOnce);
203
+ await userEvent.click(signIn);
204
+ await expect(await canvas.findByText(REDIRECT_STALL_MESSAGE)).toBeVisible();
205
+ await expect(
206
+ canvas.getByRole("button", { name: "Sign in with Microsoft" }),
207
+ ).toBeEnabled();
208
+ },
209
+ };
210
+
211
+ export const AddressAutodiscovery: Story = {
212
+ parameters: handlers({ autoconfig: "slow" }),
213
+ play: async ({ canvasElement }) => {
214
+ const canvas = within(canvasElement);
215
+ await toAddress(canvas);
216
+ await userEvent.type(await canvas.findByLabelText("Email address"), EMAIL);
217
+ await press(canvas, "Continue");
218
+ await expect(
219
+ await canvas.findByText("Looking up settings for northwind.example…"),
220
+ ).toBeVisible();
221
+ },
222
+ };
223
+
224
+ export const AddressInvalid: Story = {
225
+ play: async ({ canvasElement }) => {
226
+ const canvas = within(canvasElement);
227
+ await toAddress(canvas);
228
+ await userEvent.type(
229
+ await canvas.findByLabelText("Email address"),
230
+ "alice",
231
+ );
232
+ await press(canvas, "Continue");
233
+ await expect(
234
+ await canvas.findByText("Enter a valid email address."),
235
+ ).toBeVisible();
236
+ },
237
+ };
238
+
239
+ export const ServerConfirm: Story = {
240
+ play: async ({ canvasElement }) => {
241
+ const canvas = within(canvasElement);
242
+ await toServers(canvas);
243
+ await expect(canvas.getByText(/Found via autodiscovery/)).toBeVisible();
244
+ await expect(canvas.getAllByText("detected")).toHaveLength(2);
245
+ await expect(
246
+ canvas.getAllByDisplayValue("mail.northwind.example"),
247
+ ).toHaveLength(2);
248
+ },
249
+ };
250
+
251
+ export const ServerConfirmPhone: Story = {
252
+ globals: phone,
253
+ play: async ({ canvasElement }) => {
254
+ const canvas = within(canvasElement);
255
+ await toServers(canvas);
256
+ for (const security of canvas.getAllByLabelText("Security")) {
257
+ await expect(security).toBeEnabled();
258
+ }
259
+ },
260
+ };
261
+
262
+ export const ServerManualFallback: Story = {
263
+ parameters: handlers({ autoconfig: "missing" }),
264
+ play: async ({ canvasElement }) => {
265
+ const canvas = within(canvasElement);
266
+ await toServers(canvas);
267
+ await expect(
268
+ canvas.getByText(/couldn't detect settings for northwind.example/),
269
+ ).toBeVisible();
270
+ await expect(
271
+ canvas.getByDisplayValue("imap.northwind.example"),
272
+ ).toBeVisible();
273
+ await expect(canvas.queryByText("detected")).toBeNull();
274
+ },
275
+ };
276
+
277
+ export const ServerMissingHost: Story = {
278
+ parameters: handlers({ autoconfig: "missing" }),
279
+ play: async ({ canvasElement }) => {
280
+ const canvas = within(canvasElement);
281
+ await toServers(canvas);
282
+ await userEvent.clear(canvas.getByDisplayValue("imap.northwind.example"));
283
+ await press(canvas, "Continue");
284
+ await expect(await canvas.findByText("Enter the IMAP host.")).toBeVisible();
285
+ },
286
+ };
287
+
288
+ export const ServerProviderPreset: Story = {
289
+ play: async ({ canvasElement }) => {
290
+ const canvas = within(canvasElement);
291
+ await toServers(canvas, "alice@icloud.com");
292
+ await expect(canvas.getByLabelText("Provider")).toHaveValue("icloud");
293
+ await expect(
294
+ canvas.getByText(/pre-filled for iCloud and locked/),
295
+ ).toBeVisible();
296
+ await expect(
297
+ canvas.getByRole("link", { name: "Get an app password" }),
298
+ ).toBeVisible();
299
+ },
300
+ };
301
+
302
+ export const Credentials: Story = {
303
+ play: async ({ canvasElement }) => {
304
+ const canvas = within(canvasElement);
305
+ await toCredentials(canvas);
306
+ await expect(canvas.getByLabelText("Username")).toHaveValue(EMAIL);
307
+ await press(canvas, "Test connection");
308
+ await expect(
309
+ await canvas.findByText("Enter your username and password."),
310
+ ).toBeVisible();
311
+ },
312
+ };
313
+
314
+ export const TestConnectionSuccess: Story = {
315
+ play: async ({ canvasElement }) => {
316
+ const canvas = within(canvasElement);
317
+ await toTest(canvas);
318
+ await expect(await canvas.findByText("Connection verified")).toBeVisible();
319
+ },
320
+ };
321
+
322
+ export const TestConnectionFailure: Story = {
323
+ parameters: handlers({
324
+ connection: {
325
+ imapSuccess: true,
326
+ smtpSuccess: false,
327
+ smtpError: "535 5.7.8 Authentication credentials invalid",
328
+ },
329
+ }),
330
+ play: async ({ canvasElement }) => {
331
+ const canvas = within(canvasElement);
332
+ await toTest(canvas);
333
+ await expect(
334
+ await canvas.findByText(/many providers require an app password/),
335
+ ).toBeVisible();
336
+ await expect(
337
+ canvas.getByRole("button", { name: "Back to credentials" }),
338
+ ).toBeVisible();
339
+ },
340
+ };
341
+
342
+ export const TestConnectionNetworkFailure: Story = {
343
+ parameters: handlers({
344
+ connection: {
345
+ imapSuccess: false,
346
+ imapError: "connect ECONNREFUSED 203.0.113.7:993",
347
+ },
348
+ }),
349
+ play: async ({ canvasElement }) => {
350
+ const canvas = within(canvasElement);
351
+ await toTest(canvas);
352
+ await expect(
353
+ await canvas.findByRole("button", { name: "Back to servers" }),
354
+ ).toBeVisible();
355
+ await expect(canvas.getAllByText(/ECONNREFUSED/).length).toBeGreaterThan(0);
356
+ await expect(
357
+ canvas.queryByText(/many providers require an app password/),
358
+ ).toBeNull();
359
+ },
360
+ };
361
+
362
+ export const SyncProgress: Story = {
363
+ parameters: handlers({
364
+ sync: {
365
+ syncPhase: "syncing_inbox",
366
+ mailboxCountTotal: 6,
367
+ mailboxCountSynced: 0,
368
+ mailboxes: [
369
+ {
370
+ mailboxId: "mbx-new-inbox",
371
+ fullPath: "INBOX",
372
+ phase: "syncing",
373
+ messagesTotal: 800,
374
+ messagesSynced: 120,
375
+ highWaterMarkUid: 0,
376
+ },
377
+ ],
378
+ },
379
+ }),
380
+ play: async ({ canvasElement }) => {
381
+ const canvas = within(canvasElement);
382
+ await toTest(canvas);
383
+ await expect(
384
+ await canvas.findByText("120 / 800 messages", undefined, {
385
+ timeout: 4000,
386
+ }),
387
+ ).toBeVisible();
388
+ await expect(
389
+ canvas.getByRole("button", { name: "Go to inbox" }),
390
+ ).toBeEnabled();
391
+ },
392
+ };
393
+
394
+ const waitingForMail = handlers({
395
+ sync: { syncPhase: "discovering_mailboxes", mailboxes: [] },
396
+ });
397
+
398
+ export const SyncWaitingForFirstMail: Story = {
399
+ parameters: waitingForMail,
400
+ play: async ({ canvasElement }) => {
401
+ const canvas = within(canvasElement);
402
+ await toTest(canvas);
403
+ await expect(
404
+ await canvas.findByRole(
405
+ "button",
406
+ { name: "Getting your mail…" },
407
+ { timeout: 4000 },
408
+ ),
409
+ ).toBeDisabled();
410
+ },
411
+ };
412
+
413
+ export const SyncWaitingForFirstMailPhone: Story = {
414
+ globals: phone,
415
+ parameters: waitingForMail,
416
+ play: SyncWaitingForFirstMail.play,
417
+ };
418
+
419
+ export const SyncCreateError: Story = {
420
+ parameters: handlers({ createFails: true }),
421
+ play: async ({ canvasElement }) => {
422
+ const canvas = within(canvasElement);
423
+ await toTest(canvas);
424
+ await expect(
425
+ await canvas.findByText("Couldn't create account", undefined, {
426
+ timeout: 4000,
427
+ }),
428
+ ).toBeVisible();
429
+ await expect(canvas.getByRole("button", { name: "Retry" })).toBeVisible();
430
+ },
431
+ };
432
+
433
+ export const SyncStalled: Story = {
434
+ parameters: handlers(
435
+ { sync: { syncPhase: "error", mailboxes: [] } },
436
+ mailWorld({
437
+ accounts: [
438
+ makeAccount({
439
+ accountId: NEW_ACCOUNT_ID,
440
+ email: EMAIL,
441
+ lastError: "IMAP IDLE dropped: connection reset by peer",
442
+ }),
443
+ ],
444
+ threads: [],
445
+ outbox: [],
446
+ }),
447
+ ),
448
+ play: async ({ canvasElement }) => {
449
+ const canvas = within(canvasElement);
450
+ await toTest(canvas);
451
+ await expect(
452
+ await canvas.findByText(/Sync stalled/, undefined, { timeout: 4000 }),
453
+ ).toBeVisible();
454
+ await waitFor(() =>
455
+ expect(
456
+ canvas.getByText("IMAP IDLE dropped: connection reset by peer"),
457
+ ).toBeVisible(),
458
+ );
459
+ },
460
+ };