@remit/web-client 0.0.299 → 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 +2 -1
- package/src/auth/amplify-config.ts +2 -3
- package/src/auth/cognito-sign-in.stories.tsx +90 -0
- package/src/auth/email-sign-in.stories.tsx +128 -0
- package/src/components/mail/MailSidebarAdapter.tsx +2 -0
- package/src/components/onboarding/onboarding.stories.tsx +460 -0
- package/src/components/settings/AccountServices.render.test.ts +254 -0
- package/src/components/settings/AccountServices.tsx +181 -0
- package/src/components/settings/AccountServicesCard.stories.tsx +71 -0
- package/src/components/settings/AccountServicesCard.tsx +76 -0
- package/src/components/settings/accounts.stories.tsx +363 -0
- package/src/components/settings/advanced.stories.tsx +162 -0
- package/src/components/settings/appearance.stories.tsx +33 -0
- package/src/components/settings/folders.stories.tsx +89 -0
- package/src/components/settings/self-update.stories.tsx +270 -0
- package/src/components/settings/senders.stories.tsx +155 -0
- package/src/lib/folder-failure.test.ts +29 -0
- package/src/lib/folder-failure.ts +10 -0
- package/src/mocks/story-frame/AppStory.tsx +21 -6
- package/src/mocks/story-frame/mail-handlers.ts +69 -7
- package/src/mocks/story-frame/mail-world.ts +22 -4
- package/src/mocks/story-frame/onboarding-handlers.ts +97 -0
- package/src/mocks/story-frame/session.tsx +19 -0
- package/src/routes/settings/-accounts.test.ts +1 -0
- package/src/routes/settings/accounts.tsx +28 -20
- package/src/routes/settings/folders.tsx +2 -0
- package/src/test-support/fixtures.ts +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { RemitImapQuarantineResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { HttpResponse, http } from "msw";
|
|
4
|
+
import { expect, within } from "storybook/test";
|
|
5
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
6
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
7
|
+
import {
|
|
8
|
+
mailboxIdFor,
|
|
9
|
+
mailWorld,
|
|
10
|
+
NOW,
|
|
11
|
+
PERSONAL,
|
|
12
|
+
} from "@/mocks/story-frame/mail-world";
|
|
13
|
+
import { withRuntimeConfig } from "@/mocks/story-frame/session";
|
|
14
|
+
|
|
15
|
+
const setAside = (
|
|
16
|
+
id: string,
|
|
17
|
+
role: "Inbox" | "Archive",
|
|
18
|
+
failureCode: RemitImapQuarantineResponse["failureCode"],
|
|
19
|
+
failureMessage: string,
|
|
20
|
+
): RemitImapQuarantineResponse => ({
|
|
21
|
+
quarantineId: `q-${id}`,
|
|
22
|
+
accountConfigId: "cfg-1",
|
|
23
|
+
accountId: PERSONAL,
|
|
24
|
+
mailboxId: mailboxIdFor(PERSONAL, role),
|
|
25
|
+
uidValidity: 1_712_000_000,
|
|
26
|
+
uid: 40_217,
|
|
27
|
+
mailboxRole: role,
|
|
28
|
+
mailboxPath: role === "Inbox" ? "INBOX" : "Archive",
|
|
29
|
+
quarantinedAt: NOW,
|
|
30
|
+
attempts: 3,
|
|
31
|
+
failureStage: "BodyParse",
|
|
32
|
+
failureCode,
|
|
33
|
+
failureMessage,
|
|
34
|
+
workerVersion: "worker 1.0.0",
|
|
35
|
+
contentType: "multipart/mixed",
|
|
36
|
+
sizeBytes: 184_233,
|
|
37
|
+
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
38
|
+
createdAt: NOW,
|
|
39
|
+
updatedAt: NOW,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const unreadableBody = setAside(
|
|
43
|
+
"1",
|
|
44
|
+
"Inbox",
|
|
45
|
+
"UnreadableBody",
|
|
46
|
+
"multipart boundary was never closed",
|
|
47
|
+
);
|
|
48
|
+
const unknownCharset = setAside(
|
|
49
|
+
"2",
|
|
50
|
+
"Archive",
|
|
51
|
+
"UnknownCharset",
|
|
52
|
+
"declared charset is not a known encoding",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const meta = {
|
|
56
|
+
title: "Playground/Shipped/Settings/Advanced/Overview",
|
|
57
|
+
component: AppStory,
|
|
58
|
+
args: { url: "/settings/advanced" },
|
|
59
|
+
parameters: {
|
|
60
|
+
layout: "fullscreen",
|
|
61
|
+
msw: { handlers: mailHandlers(mailWorld()) },
|
|
62
|
+
},
|
|
63
|
+
} satisfies Meta<typeof AppStory>;
|
|
64
|
+
export default meta;
|
|
65
|
+
|
|
66
|
+
type Story = StoryObj<typeof meta>;
|
|
67
|
+
|
|
68
|
+
export const Advanced: Story = {
|
|
69
|
+
play: async ({ canvasElement }) => {
|
|
70
|
+
const canvas = within(canvasElement);
|
|
71
|
+
await expect(
|
|
72
|
+
await canvas.findByText(/Every message has been read successfully/),
|
|
73
|
+
).toBeVisible();
|
|
74
|
+
await expect(canvas.getByText("About")).toBeVisible();
|
|
75
|
+
await expect(
|
|
76
|
+
canvas.queryByRole("link", { name: "Download root certificate" }),
|
|
77
|
+
).toBeNull();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const AdvancedOneQuarantined: Story = {
|
|
82
|
+
parameters: {
|
|
83
|
+
msw: {
|
|
84
|
+
handlers: mailHandlers(mailWorld({ quarantine: [unreadableBody] })),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
play: async ({ canvasElement }) => {
|
|
88
|
+
const canvas = within(canvasElement);
|
|
89
|
+
await expect(
|
|
90
|
+
await canvas.findByText(/multipart boundary was never closed/),
|
|
91
|
+
).toBeVisible();
|
|
92
|
+
await expect(canvas.queryByText(/messages could not be read/)).toBeNull();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const AdvancedQuarantineAlert: Story = {
|
|
97
|
+
parameters: {
|
|
98
|
+
msw: {
|
|
99
|
+
handlers: mailHandlers(
|
|
100
|
+
mailWorld({ quarantine: [unreadableBody, unknownCharset] }),
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
play: async ({ canvasElement }) => {
|
|
105
|
+
const canvas = within(canvasElement);
|
|
106
|
+
await expect(
|
|
107
|
+
await canvas.findByText("2 messages could not be read."),
|
|
108
|
+
).toBeVisible();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const AdvancedQuarantineLoading: Story = {
|
|
113
|
+
parameters: {
|
|
114
|
+
msw: {
|
|
115
|
+
handlers: [
|
|
116
|
+
http.get(
|
|
117
|
+
"/api/me/quarantine",
|
|
118
|
+
() => new Promise<never>(() => undefined),
|
|
119
|
+
),
|
|
120
|
+
...mailHandlers(mailWorld()),
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
play: async ({ canvasElement }) => {
|
|
125
|
+
const canvas = within(canvasElement);
|
|
126
|
+
await expect(
|
|
127
|
+
await canvas.findByText("Checking for messages set aside…"),
|
|
128
|
+
).toBeVisible();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const AdvancedQuarantineUnreadable: Story = {
|
|
133
|
+
parameters: {
|
|
134
|
+
msw: {
|
|
135
|
+
handlers: [
|
|
136
|
+
http.get("/api/me/quarantine", () => HttpResponse.error()),
|
|
137
|
+
...mailHandlers(mailWorld()),
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
play: async ({ canvasElement }) => {
|
|
142
|
+
const canvas = within(canvasElement);
|
|
143
|
+
await expect(
|
|
144
|
+
await canvas.findByText(
|
|
145
|
+
"The list of set-aside messages could not be read.",
|
|
146
|
+
),
|
|
147
|
+
).toBeVisible();
|
|
148
|
+
await expect(
|
|
149
|
+
canvas.getByRole("button", { name: "Report this" }),
|
|
150
|
+
).toBeVisible();
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const AdvancedTlsInternal: Story = {
|
|
155
|
+
beforeEach: withRuntimeConfig({ tlsMode: "internal" }),
|
|
156
|
+
play: async ({ canvasElement }) => {
|
|
157
|
+
const canvas = within(canvasElement);
|
|
158
|
+
await expect(
|
|
159
|
+
await canvas.findByRole("link", { name: "Download root certificate" }),
|
|
160
|
+
).toHaveAttribute("href", "/tls-root-ca.crt");
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
4
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
5
|
+
import { mailWorld } from "@/mocks/story-frame/mail-world";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Playground/Shipped/Settings/Appearance",
|
|
9
|
+
component: AppStory,
|
|
10
|
+
args: { url: "/settings/appearance" },
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "fullscreen",
|
|
13
|
+
msw: { handlers: mailHandlers(mailWorld()) },
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof AppStory>;
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Appearance: Story = {
|
|
21
|
+
play: async ({ canvasElement }) => {
|
|
22
|
+
const canvas = within(canvasElement);
|
|
23
|
+
const density = await canvas.findByRole("radiogroup", { name: "Density" });
|
|
24
|
+
await expect(
|
|
25
|
+
within(density).getByRole("radio", { name: "Comfortable" }),
|
|
26
|
+
).toBeChecked();
|
|
27
|
+
const theme = canvas.getByRole("radiogroup", { name: "Theme" });
|
|
28
|
+
await expect(within(theme).getAllByRole("radio")).toHaveLength(3);
|
|
29
|
+
await expect(
|
|
30
|
+
canvas.getByText(/System default follows your OS preference/),
|
|
31
|
+
).toBeVisible();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
4
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
5
|
+
import {
|
|
6
|
+
accountWithFolders,
|
|
7
|
+
foldersOf,
|
|
8
|
+
mailWorld,
|
|
9
|
+
} from "@/mocks/story-frame/mail-world";
|
|
10
|
+
import { makeMailbox } from "@/test-support/fixtures";
|
|
11
|
+
|
|
12
|
+
const ACCOUNT_ID = "acc-work";
|
|
13
|
+
const EMAIL = "alice@northwind.example";
|
|
14
|
+
|
|
15
|
+
const folder = (path: string, messageCount: number) =>
|
|
16
|
+
makeMailbox({
|
|
17
|
+
accountId: ACCOUNT_ID,
|
|
18
|
+
mailboxId: `mbx-${path.toLowerCase().replaceAll("/", "-")}`,
|
|
19
|
+
fullPath: path,
|
|
20
|
+
messageCount,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const world = mailWorld({
|
|
24
|
+
accounts: [accountWithFolders(ACCOUNT_ID, EMAIL, "Work")],
|
|
25
|
+
mailboxes: [
|
|
26
|
+
...foldersOf(ACCOUNT_ID),
|
|
27
|
+
folder("Travel", 0),
|
|
28
|
+
folder("Travel/Flights", 41),
|
|
29
|
+
folder("Travel/Hotels", 96),
|
|
30
|
+
folder("Finance", 0),
|
|
31
|
+
folder("Finance/Invoices", 12),
|
|
32
|
+
folder("Family", 230),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const meta = {
|
|
37
|
+
title: "Playground/Shipped/Settings/Folder roles",
|
|
38
|
+
component: AppStory,
|
|
39
|
+
args: { url: "/settings/folders" },
|
|
40
|
+
parameters: {
|
|
41
|
+
layout: "fullscreen",
|
|
42
|
+
msw: { handlers: mailHandlers(world) },
|
|
43
|
+
},
|
|
44
|
+
} satisfies Meta<typeof AppStory>;
|
|
45
|
+
export default meta;
|
|
46
|
+
|
|
47
|
+
type Story = StoryObj<typeof meta>;
|
|
48
|
+
|
|
49
|
+
const page = within(document.body);
|
|
50
|
+
|
|
51
|
+
export const Desktop: Story = {
|
|
52
|
+
globals: { viewport: { value: "desktop", isRotated: false } },
|
|
53
|
+
play: async ({ canvasElement }) => {
|
|
54
|
+
const canvas = within(canvasElement);
|
|
55
|
+
await expect(
|
|
56
|
+
await canvas.findByRole("heading", { name: `Your folders — ${EMAIL}` }),
|
|
57
|
+
).toBeVisible();
|
|
58
|
+
await expect(
|
|
59
|
+
canvas.getByRole("tree", { name: `All folders for ${EMAIL}` }),
|
|
60
|
+
).toBeVisible();
|
|
61
|
+
await expect(
|
|
62
|
+
canvas.getByRole("button", { name: "Rename Travel" }),
|
|
63
|
+
).toBeInTheDocument();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Phone: Story = {
|
|
68
|
+
globals: { viewport: { value: "mobile", isRotated: false } },
|
|
69
|
+
play: async ({ canvasElement }) => {
|
|
70
|
+
const canvas = within(canvasElement);
|
|
71
|
+
await expect(
|
|
72
|
+
await canvas.findByRole("tree", { name: `All folders for ${EMAIL}` }),
|
|
73
|
+
).toBeVisible();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Renaming: Story = {
|
|
78
|
+
globals: { viewport: { value: "desktop", isRotated: false } },
|
|
79
|
+
play: async ({ canvasElement }) => {
|
|
80
|
+
const canvas = within(canvasElement);
|
|
81
|
+
await userEvent.click(
|
|
82
|
+
await canvas.findByRole("button", { name: "Rename Travel" }),
|
|
83
|
+
);
|
|
84
|
+
const dialog = within(
|
|
85
|
+
await page.findByRole("dialog", { name: "Rename Travel" }),
|
|
86
|
+
);
|
|
87
|
+
await expect(dialog.getByRole("textbox")).toBeVisible();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapSystemUpdateCheck,
|
|
3
|
+
RemitImapSystemUpdateResponse,
|
|
4
|
+
RemitImapSystemUpdateRun,
|
|
5
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
7
|
+
import { HttpResponse, http } from "msw";
|
|
8
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
9
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
10
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
11
|
+
import { mailWorld } from "@/mocks/story-frame/mail-world";
|
|
12
|
+
|
|
13
|
+
const CURRENT = "0.9.3";
|
|
14
|
+
const NEXT = "1.0.0";
|
|
15
|
+
const LOGS = "remit logs updater --since 2026-09-24T12:40";
|
|
16
|
+
|
|
17
|
+
const checked: RemitImapSystemUpdateCheck = {
|
|
18
|
+
status: "ok",
|
|
19
|
+
lastCheckedAt: "2026-09-24T12:39:00Z",
|
|
20
|
+
latestVersion: CURRENT,
|
|
21
|
+
updateAvailable: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const released: RemitImapSystemUpdateCheck = {
|
|
25
|
+
status: "ok",
|
|
26
|
+
lastCheckedAt: "2026-09-24T12:39:00Z",
|
|
27
|
+
latestVersion: NEXT,
|
|
28
|
+
publishedAt: "2026-09-22T09:00:00Z",
|
|
29
|
+
summary: "Faster search and a calmer daily brief.",
|
|
30
|
+
releaseNotesUrl: `https://github.com/remit-mail/reader/releases/tag/${NEXT}`,
|
|
31
|
+
updateAvailable: true,
|
|
32
|
+
schemaVersion: 42,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const run = (
|
|
36
|
+
outcome: RemitImapSystemUpdateRun["outcome"],
|
|
37
|
+
message: string,
|
|
38
|
+
startedAt = "2026-09-24T12:40:00Z",
|
|
39
|
+
): RemitImapSystemUpdateRun => ({
|
|
40
|
+
runId: "run-1",
|
|
41
|
+
fromVersion: CURRENT,
|
|
42
|
+
targetVersion: NEXT,
|
|
43
|
+
phase: outcome === null ? "verifying" : "committing",
|
|
44
|
+
outcome,
|
|
45
|
+
startedAt,
|
|
46
|
+
updatedAt: startedAt,
|
|
47
|
+
message,
|
|
48
|
+
logCommand: LOGS,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const update = (
|
|
52
|
+
overrides: Partial<RemitImapSystemUpdateResponse>,
|
|
53
|
+
): RemitImapSystemUpdateResponse => ({
|
|
54
|
+
currentVersion: CURRENT,
|
|
55
|
+
currentSchemaVersion: 42,
|
|
56
|
+
check: checked,
|
|
57
|
+
run: null,
|
|
58
|
+
...overrides,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const answering = (systemUpdate: RemitImapSystemUpdateResponse) => ({
|
|
62
|
+
msw: { handlers: mailHandlers(mailWorld({ systemUpdate })) },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const meta = {
|
|
66
|
+
title: "Playground/Shipped/Settings/Advanced/Self-update",
|
|
67
|
+
component: AppStory,
|
|
68
|
+
args: { url: "/settings/advanced" },
|
|
69
|
+
parameters: { layout: "fullscreen", ...answering(update({})) },
|
|
70
|
+
} satisfies Meta<typeof AppStory>;
|
|
71
|
+
export default meta;
|
|
72
|
+
|
|
73
|
+
type Story = StoryObj<typeof meta>;
|
|
74
|
+
|
|
75
|
+
const page = within(document.body);
|
|
76
|
+
|
|
77
|
+
export const AdvancedUpToDate: Story = {
|
|
78
|
+
play: async ({ canvasElement }) => {
|
|
79
|
+
const canvas = within(canvasElement);
|
|
80
|
+
await expect(
|
|
81
|
+
await canvas.findByText(`Remit ${CURRENT} is the latest version.`),
|
|
82
|
+
).toBeVisible();
|
|
83
|
+
await expect(
|
|
84
|
+
canvas.getByRole("button", { name: "Check again" }),
|
|
85
|
+
).toBeVisible();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const AdvancedNeverChecked: Story = {
|
|
90
|
+
parameters: answering(update({ check: { status: "disabled" } })),
|
|
91
|
+
play: async ({ canvasElement }) => {
|
|
92
|
+
const canvas = within(canvasElement);
|
|
93
|
+
await expect(
|
|
94
|
+
await canvas.findByText(/No update check has run yet/),
|
|
95
|
+
).toBeVisible();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const AdvancedUpdateAvailable: Story = {
|
|
100
|
+
parameters: answering(update({ check: released })),
|
|
101
|
+
play: async ({ canvasElement }) => {
|
|
102
|
+
const canvas = within(canvasElement);
|
|
103
|
+
await expect(
|
|
104
|
+
await canvas.findByRole("button", { name: `Install ${NEXT}` }),
|
|
105
|
+
).toBeVisible();
|
|
106
|
+
await expect(canvas.getByText("update available")).toBeVisible();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const AdvancedCheckFailed: Story = {
|
|
111
|
+
parameters: answering(
|
|
112
|
+
update({
|
|
113
|
+
check: {
|
|
114
|
+
status: "failed",
|
|
115
|
+
lastCheckedAt: "2026-09-21T12:00:00Z",
|
|
116
|
+
error: "No route to github.com — the server has no outbound network.",
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
120
|
+
play: async ({ canvasElement }) => {
|
|
121
|
+
const canvas = within(canvasElement);
|
|
122
|
+
await expect(
|
|
123
|
+
await canvas.findByText("Could not reach the update source."),
|
|
124
|
+
).toBeVisible();
|
|
125
|
+
await expect(canvas.getByText(/No route to github.com/)).toBeVisible();
|
|
126
|
+
await expect(
|
|
127
|
+
canvas.getByRole("button", { name: "Try again" }),
|
|
128
|
+
).toBeVisible();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const AdvancedSucceeded: Story = {
|
|
133
|
+
parameters: answering(
|
|
134
|
+
update({ currentVersion: NEXT, run: run("succeeded", "Updated") }),
|
|
135
|
+
),
|
|
136
|
+
play: async ({ canvasElement }) => {
|
|
137
|
+
const canvas = within(canvasElement);
|
|
138
|
+
await expect(
|
|
139
|
+
await canvas.findByText(`Updated to Remit ${NEXT}.`),
|
|
140
|
+
).toBeVisible();
|
|
141
|
+
await expect(canvas.getByText(`Remit ${NEXT} is running.`)).toBeVisible();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const AdvancedRolledBack: Story = {
|
|
146
|
+
parameters: answering(
|
|
147
|
+
update({
|
|
148
|
+
run: run(
|
|
149
|
+
"rolledBack",
|
|
150
|
+
'migration 0042_add_thread_index failed: relation "threads" does not exist',
|
|
151
|
+
),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
play: async ({ canvasElement }) => {
|
|
155
|
+
const canvas = within(canvasElement);
|
|
156
|
+
await expect(
|
|
157
|
+
await canvas.findByText(/relation "threads" does not exist/),
|
|
158
|
+
).toBeVisible();
|
|
159
|
+
await expect(canvas.getByText(LOGS)).toBeVisible();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const AdvancedRollbackFailed: Story = {
|
|
164
|
+
parameters: answering(
|
|
165
|
+
update({
|
|
166
|
+
run: run(
|
|
167
|
+
"rollbackFailed",
|
|
168
|
+
"migration 0042_add_thread_index failed and the snapshot restore errored: database is locked",
|
|
169
|
+
),
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
play: async ({ canvasElement }) => {
|
|
173
|
+
const canvas = within(canvasElement);
|
|
174
|
+
await expect(
|
|
175
|
+
await canvas.findByText(/the snapshot restore errored/),
|
|
176
|
+
).toBeVisible();
|
|
177
|
+
await expect(canvas.getByText(LOGS)).toBeVisible();
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const AdvancedAbandoned: Story = {
|
|
182
|
+
parameters: answering(
|
|
183
|
+
update({
|
|
184
|
+
run: run(
|
|
185
|
+
"abandoned",
|
|
186
|
+
"manifest fetch timed out before anything was pulled",
|
|
187
|
+
),
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
play: async ({ canvasElement }) => {
|
|
191
|
+
const canvas = within(canvasElement);
|
|
192
|
+
await expect(
|
|
193
|
+
await canvas.findByText(/manifest fetch timed out/),
|
|
194
|
+
).toBeVisible();
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const ConsentWithSchemaMigration: Story = {
|
|
199
|
+
parameters: answering(update({ currentSchemaVersion: 41, check: released })),
|
|
200
|
+
play: async ({ canvasElement }) => {
|
|
201
|
+
const canvas = within(canvasElement);
|
|
202
|
+
await userEvent.click(
|
|
203
|
+
await canvas.findByRole("button", { name: `Install ${NEXT}` }),
|
|
204
|
+
);
|
|
205
|
+
const dialog = within(
|
|
206
|
+
await page.findByRole("dialog", { name: `Install Remit ${NEXT}` }),
|
|
207
|
+
);
|
|
208
|
+
await expect(
|
|
209
|
+
dialog.getByText(/updates the database while Remit is offline/),
|
|
210
|
+
).toBeVisible();
|
|
211
|
+
await expect(
|
|
212
|
+
dialog.getByRole("button", { name: "Install and restart" }),
|
|
213
|
+
).toBeVisible();
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const RestartInProgress: Story = {
|
|
218
|
+
parameters: answering(
|
|
219
|
+
update({
|
|
220
|
+
run: run(null, "", new Date(Date.now() - 52_000).toISOString()),
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
play: async () => {
|
|
224
|
+
await expect(
|
|
225
|
+
await page.findByRole("heading", { name: `Installing Remit ${NEXT}` }),
|
|
226
|
+
).toBeVisible();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
let answered = false;
|
|
231
|
+
|
|
232
|
+
const realNow = Date.now;
|
|
233
|
+
|
|
234
|
+
const clockAhead = (ms: number) => {
|
|
235
|
+
Date.now = () => realNow.call(Date) + ms;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const ServerNeverCameBack: Story = {
|
|
239
|
+
parameters: {
|
|
240
|
+
msw: {
|
|
241
|
+
handlers: [
|
|
242
|
+
http.get("/api/system/update", () => {
|
|
243
|
+
if (answered) return HttpResponse.error();
|
|
244
|
+
answered = true;
|
|
245
|
+
return HttpResponse.json<RemitImapSystemUpdateResponse>(
|
|
246
|
+
update({ run: run(null, "", new Date().toISOString()) }),
|
|
247
|
+
);
|
|
248
|
+
}),
|
|
249
|
+
...mailHandlers(mailWorld()),
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
beforeEach: () => {
|
|
254
|
+
answered = false;
|
|
255
|
+
return () => {
|
|
256
|
+
Date.now = realNow;
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
play: async () => {
|
|
260
|
+
await page.findByRole("heading", { name: `Installing Remit ${NEXT}` });
|
|
261
|
+
clockAhead(20 * 60_000);
|
|
262
|
+
await waitFor(
|
|
263
|
+
() =>
|
|
264
|
+
expect(page.getByRole("alertdialog")).toHaveTextContent(
|
|
265
|
+
"Your mail is safe at your provider",
|
|
266
|
+
),
|
|
267
|
+
{ timeout: 10_000 },
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapAddressResponse,
|
|
3
|
+
RemitImapVipSuggestionEntry,
|
|
4
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
6
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
7
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
8
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
9
|
+
import { mailWorld, NOW } from "@/mocks/story-frame/mail-world";
|
|
10
|
+
|
|
11
|
+
const suggestion = (
|
|
12
|
+
addressId: string,
|
|
13
|
+
displayName: string,
|
|
14
|
+
normalizedEmail: string,
|
|
15
|
+
inboundCount: number,
|
|
16
|
+
replyCount: number,
|
|
17
|
+
): RemitImapVipSuggestionEntry => ({
|
|
18
|
+
addressId,
|
|
19
|
+
displayName,
|
|
20
|
+
normalizedEmail,
|
|
21
|
+
inboundCount,
|
|
22
|
+
outboundCount: replyCount,
|
|
23
|
+
replyCount,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const flagged = (
|
|
27
|
+
addressId: string,
|
|
28
|
+
displayName: string,
|
|
29
|
+
normalizedEmail: string,
|
|
30
|
+
flags: RemitImapAddressResponse["flags"],
|
|
31
|
+
): RemitImapAddressResponse => {
|
|
32
|
+
const [localPart = "", domain = ""] = normalizedEmail.split("@");
|
|
33
|
+
return {
|
|
34
|
+
addressId,
|
|
35
|
+
accountConfigId: "cfg-1",
|
|
36
|
+
displayName,
|
|
37
|
+
localPart,
|
|
38
|
+
domain,
|
|
39
|
+
normalizedEmail,
|
|
40
|
+
flags,
|
|
41
|
+
inboundCount: 0,
|
|
42
|
+
outboundCount: 0,
|
|
43
|
+
replyCount: 0,
|
|
44
|
+
lastInboundAt: NOW,
|
|
45
|
+
lastReplyAt: 0,
|
|
46
|
+
createdAt: NOW,
|
|
47
|
+
updatedAt: NOW,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const world = mailWorld({
|
|
52
|
+
vipSuggestions: [
|
|
53
|
+
suggestion("addr-ada", "Ada Lovelace", "ada@acme.example", 48, 21),
|
|
54
|
+
suggestion("addr-grace", "Grace Hopper", "grace@example.org", 12, 4),
|
|
55
|
+
suggestion("addr-linus", "Linus", "linus@example.org", 7, 0),
|
|
56
|
+
],
|
|
57
|
+
addresses: [
|
|
58
|
+
flagged("addr-weekly", "The Weekly", "digest@weekly.example", {
|
|
59
|
+
muted: { value: true, setAt: NOW, reason: "too frequent" },
|
|
60
|
+
}),
|
|
61
|
+
flagged("addr-outdoor", "Outdoor Store", "news@outdoor.example", {
|
|
62
|
+
muted: { value: true, setAt: NOW },
|
|
63
|
+
}),
|
|
64
|
+
flagged("addr-spam", "Prize Desk", "win@prize.example", {
|
|
65
|
+
blocked: { value: true, setAt: NOW },
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const meta = {
|
|
71
|
+
title: "Playground/Shipped/Settings/Senders",
|
|
72
|
+
component: AppStory,
|
|
73
|
+
args: { url: "/settings/senders" },
|
|
74
|
+
parameters: {
|
|
75
|
+
layout: "fullscreen",
|
|
76
|
+
msw: { handlers: mailHandlers(world) },
|
|
77
|
+
},
|
|
78
|
+
} satisfies Meta<typeof AppStory>;
|
|
79
|
+
export default meta;
|
|
80
|
+
|
|
81
|
+
type Story = StoryObj<typeof meta>;
|
|
82
|
+
|
|
83
|
+
export const SendersAndRules: Story = {
|
|
84
|
+
play: async ({ canvasElement }) => {
|
|
85
|
+
const canvas = within(canvasElement);
|
|
86
|
+
await expect(await canvas.findByText("Ada Lovelace")).toBeVisible();
|
|
87
|
+
await expect(
|
|
88
|
+
canvas.getByText("48 received · you replied 21×"),
|
|
89
|
+
).toBeVisible();
|
|
90
|
+
await expect(
|
|
91
|
+
canvas.getByText("7 received · you've never replied"),
|
|
92
|
+
).toBeVisible();
|
|
93
|
+
await expect(
|
|
94
|
+
canvas.getAllByRole("button", { name: "Add VIP" }),
|
|
95
|
+
).toHaveLength(3);
|
|
96
|
+
await expect(canvas.getByText("3 of 3 flagged senders")).toBeVisible();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const FilteredVips: Story = {
|
|
101
|
+
play: async ({ canvasElement }) => {
|
|
102
|
+
const canvas = within(canvasElement);
|
|
103
|
+
await canvas.findByText("Ada Lovelace");
|
|
104
|
+
await userEvent.type(
|
|
105
|
+
canvas.getByPlaceholderText("Filter vips by name or address"),
|
|
106
|
+
"grace",
|
|
107
|
+
);
|
|
108
|
+
await expect(
|
|
109
|
+
await canvas.findByText("1 of 3 flagged senders"),
|
|
110
|
+
).toBeVisible();
|
|
111
|
+
await expect(canvas.queryByText("Ada Lovelace")).toBeNull();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const MutedSearch: Story = {
|
|
116
|
+
play: async ({ canvasElement }) => {
|
|
117
|
+
const canvas = within(canvasElement);
|
|
118
|
+
await canvas.findByText("Ada Lovelace");
|
|
119
|
+
await userEvent.click(canvas.getByRole("tab", { name: /^Muted/ }));
|
|
120
|
+
await expect(
|
|
121
|
+
canvas.getByText(/Type 2\+ characters to search muted senders/),
|
|
122
|
+
).toBeVisible();
|
|
123
|
+
await userEvent.type(
|
|
124
|
+
canvas.getByPlaceholderText("Filter muted by name or address"),
|
|
125
|
+
"example",
|
|
126
|
+
);
|
|
127
|
+
await expect(await canvas.findByText("The Weekly")).toBeVisible();
|
|
128
|
+
await expect(canvas.getByText("Outdoor Store")).toBeVisible();
|
|
129
|
+
await expect(canvas.queryByText("Prize Desk")).toBeNull();
|
|
130
|
+
await expect(
|
|
131
|
+
canvas.getAllByRole("button", { name: "Remove muted flag" }),
|
|
132
|
+
).toHaveLength(2);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const Empty: Story = {
|
|
137
|
+
parameters: {
|
|
138
|
+
msw: { handlers: mailHandlers(mailWorld()) },
|
|
139
|
+
},
|
|
140
|
+
play: async ({ canvasElement }) => {
|
|
141
|
+
const canvas = within(canvasElement);
|
|
142
|
+
await expect(
|
|
143
|
+
await canvas.findByText(/No VIP suggestions yet/),
|
|
144
|
+
).toBeVisible();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const Phone: Story = {
|
|
149
|
+
globals: { viewport: { value: "mobile", isRotated: false } },
|
|
150
|
+
play: async ({ canvasElement }) => {
|
|
151
|
+
const canvas = within(canvasElement);
|
|
152
|
+
await expect(await canvas.findByText("Ada Lovelace")).toBeVisible();
|
|
153
|
+
await expect(canvas.getByRole("tab", { name: /^VIPs/ })).toBeVisible();
|
|
154
|
+
},
|
|
155
|
+
};
|