@remit/web-client 0.0.302 → 0.0.304
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 +1 -1
- package/src/components/calendar/CalendarEventPane.stories.tsx +51 -0
- package/src/components/calendar/CalendarEventPane.tsx +79 -14
- package/src/components/calendar/OpenCalendarEvent.stories.tsx +128 -10
- package/src/components/calendar/OpenCalendarEvent.tsx +116 -2
- package/src/components/calendar/calendar-story-fixtures.ts +1 -0
- package/src/components/compose/compose.stories.tsx +374 -0
- package/src/components/layout/ComposeFab.tsx +2 -15
- package/src/components/layout/mail-shell.stories.tsx +142 -0
- package/src/components/mail/detail-surface.stories.tsx +171 -0
- package/src/components/mail/mail-pickers.stories.tsx +82 -0
- package/src/components/mail/search.stories.tsx +280 -0
- package/src/components/mail/selection-wizard.stories.tsx +160 -0
- package/src/hooks/calendar/index.ts +4 -0
- package/src/hooks/calendar/instance.test.ts +137 -1
- package/src/hooks/calendar/instance.ts +68 -0
- package/src/hooks/calendar/useCalendarJump.ts +117 -0
- package/src/lib/calendar-report.ts +10 -0
- package/src/lib/calendar-route.ts +2 -0
- package/src/mocks/story-frame/compose-world.ts +191 -0
- package/src/mocks/story-frame/mail-handlers.ts +195 -22
- package/src/mocks/story-frame/mail-world.ts +44 -18
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NO_QUOTABLE_BODY_FORWARD_MESSAGE,
|
|
3
|
+
SMTP_MISSING_MESSAGE,
|
|
4
|
+
} from "@remit/ui";
|
|
5
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
6
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
7
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
8
|
+
import {
|
|
9
|
+
composeWorld,
|
|
10
|
+
INBOX,
|
|
11
|
+
LONG_SUBJECT,
|
|
12
|
+
LUNCH_BODY,
|
|
13
|
+
LUNCH_SUBJECT,
|
|
14
|
+
messageUrl,
|
|
15
|
+
PLAIN_DRAFT,
|
|
16
|
+
SCANS_SUBJECT,
|
|
17
|
+
TURNS_SUBJECT,
|
|
18
|
+
threadUrl,
|
|
19
|
+
withoutSmtp,
|
|
20
|
+
} from "@/mocks/story-frame/compose-world";
|
|
21
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
22
|
+
|
|
23
|
+
const world = composeWorld();
|
|
24
|
+
const phone = { viewport: { value: "mobile", isRotated: false } };
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: "Playground/Shipped/Mail/Compose",
|
|
28
|
+
component: AppStory,
|
|
29
|
+
args: { url: `/mail/${INBOX}/compose` },
|
|
30
|
+
parameters: {
|
|
31
|
+
layout: "fullscreen",
|
|
32
|
+
msw: { handlers: mailHandlers(world) },
|
|
33
|
+
},
|
|
34
|
+
} satisfies Meta<typeof AppStory>;
|
|
35
|
+
export default meta;
|
|
36
|
+
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
const page = () => within(document.body);
|
|
40
|
+
|
|
41
|
+
const writingSurface = async (holding: string): Promise<HTMLElement> =>
|
|
42
|
+
waitFor(() => {
|
|
43
|
+
const editable = document.querySelector<HTMLElement>(
|
|
44
|
+
"[data-testid=compose-body]",
|
|
45
|
+
);
|
|
46
|
+
if (!editable?.textContent?.includes(holding))
|
|
47
|
+
throw new Error(`the editor does not hold "${holding}" yet`);
|
|
48
|
+
return editable;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const spellMarks = (editable: HTMLElement): AbstractRange[] => {
|
|
52
|
+
const ranges: AbstractRange[] = [];
|
|
53
|
+
CSS.highlights.forEach((highlight, name) => {
|
|
54
|
+
if (name !== "spell-error") return;
|
|
55
|
+
highlight.forEach((range) => {
|
|
56
|
+
if (editable.contains(range.startContainer)) ranges.push(range);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return ranges;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const refusal = async (reason: string) => {
|
|
63
|
+
const title = await page().findByText("Can't send yet");
|
|
64
|
+
await expect(title.parentElement).toHaveTextContent(reason);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const sendFromWork = async () =>
|
|
68
|
+
userEvent.selectOptions(
|
|
69
|
+
await page().findByLabelText("From:"),
|
|
70
|
+
"alice.tan@acme.example",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const composeFirst = (compose: HTMLElement, message: HTMLElement) =>
|
|
74
|
+
expect(
|
|
75
|
+
compose.compareDocumentPosition(message) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
76
|
+
).toBeTruthy();
|
|
77
|
+
|
|
78
|
+
export const Blank: Story = {
|
|
79
|
+
play: async () => {
|
|
80
|
+
await expect(
|
|
81
|
+
await page().findByRole("heading", { name: "New Message" }),
|
|
82
|
+
).toBeVisible();
|
|
83
|
+
await expect(page().getByLabelText("To:")).toHaveValue("");
|
|
84
|
+
await expect(page().getByPlaceholderText("Subject")).toHaveValue("");
|
|
85
|
+
await expect(page().getByRole("button", { name: "Send" })).toBeVisible();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Autosaves: Story = {
|
|
90
|
+
play: async () => {
|
|
91
|
+
await sendFromWork();
|
|
92
|
+
const to = await page().findByLabelText("To:");
|
|
93
|
+
await userEvent.type(to, "ada@acme.example,");
|
|
94
|
+
await userEvent.type(page().getByPlaceholderText("Subject"), "Numbers");
|
|
95
|
+
await expect(
|
|
96
|
+
await page().findByText("Draft saved", {}, { timeout: 5000 }),
|
|
97
|
+
).toBeVisible();
|
|
98
|
+
await expect(
|
|
99
|
+
page().getByRole("heading", { name: "Draft" }),
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const Spellcheck: Story = {
|
|
105
|
+
args: { url: `/mail/${INBOX}/compose/out-misspelt` },
|
|
106
|
+
play: async () => {
|
|
107
|
+
const editable = await writingSurface("Ths report");
|
|
108
|
+
await waitFor(
|
|
109
|
+
async () => {
|
|
110
|
+
await expect(spellMarks(editable).length).toBeGreaterThan(0);
|
|
111
|
+
await expect(editable).toHaveAttribute("spellcheck", "false");
|
|
112
|
+
},
|
|
113
|
+
{ timeout: 5000 },
|
|
114
|
+
);
|
|
115
|
+
await userEvent.click(page().getByTestId("compose-language-chip"));
|
|
116
|
+
const dutch = await waitFor(() => {
|
|
117
|
+
const row = document.querySelector<HTMLElement>(
|
|
118
|
+
'[role="menuitemradio"][lang="nl"]',
|
|
119
|
+
);
|
|
120
|
+
if (!row) throw new Error("the language menu is not open");
|
|
121
|
+
return row;
|
|
122
|
+
});
|
|
123
|
+
await userEvent.click(dutch);
|
|
124
|
+
await waitFor(
|
|
125
|
+
async () => {
|
|
126
|
+
await expect(editable).toHaveAttribute("spellcheck", "true");
|
|
127
|
+
await expect(spellMarks(editable)).toHaveLength(0);
|
|
128
|
+
},
|
|
129
|
+
{ timeout: 5000 },
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const OverAnOpenMessage: Story = {
|
|
135
|
+
args: { url: messageUrl("q3-planning") },
|
|
136
|
+
play: async () => {
|
|
137
|
+
const conversation = () =>
|
|
138
|
+
page().queryByRole("heading", { level: 1, name: "Q3 planning" });
|
|
139
|
+
await waitFor(() => expect(conversation()).toBeVisible());
|
|
140
|
+
|
|
141
|
+
await userEvent.click(page().getByRole("button", { name: "Compose" }));
|
|
142
|
+
const to = await page().findByLabelText("To:");
|
|
143
|
+
await expect(to).toBeVisible();
|
|
144
|
+
await expect(conversation()).toBeNull();
|
|
145
|
+
|
|
146
|
+
await userEvent.type(to, "ada@acme.example");
|
|
147
|
+
const search = page().getByLabelText("Search mail");
|
|
148
|
+
await userEvent.type(search, "invoice");
|
|
149
|
+
|
|
150
|
+
await expect(search).toHaveFocus();
|
|
151
|
+
await expect(page().getAllByLabelText("To:")).toHaveLength(1);
|
|
152
|
+
await waitFor(() =>
|
|
153
|
+
expect(
|
|
154
|
+
page().getByRole("button", { name: "Remove ada@acme.example" }),
|
|
155
|
+
).toBeVisible(),
|
|
156
|
+
);
|
|
157
|
+
await expect(conversation()).toBeNull();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const Reply: Story = {
|
|
162
|
+
args: { url: `${messageUrl("lunch")}/reply` },
|
|
163
|
+
play: async () => {
|
|
164
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
165
|
+
await waitFor(() => expect(subject).toHaveValue(`Re: ${LUNCH_SUBJECT}`));
|
|
166
|
+
await expect(
|
|
167
|
+
page().getByRole("button", { name: "Remove ada@acme.example" }),
|
|
168
|
+
).toBeVisible();
|
|
169
|
+
const messages = page().getByTestId("conversation-messages");
|
|
170
|
+
await composeFirst(subject, messages);
|
|
171
|
+
await expect(
|
|
172
|
+
(await within(messages).findAllByText(LUNCH_BODY))[0],
|
|
173
|
+
).toBeVisible();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const Forward: Story = {
|
|
178
|
+
args: { url: `${messageUrl("lunch")}/forward` },
|
|
179
|
+
play: async () => {
|
|
180
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
181
|
+
await waitFor(() => expect(subject).toHaveValue(`Fwd: ${LUNCH_SUBJECT}`));
|
|
182
|
+
await expect(page().getByLabelText("To:")).toHaveValue("");
|
|
183
|
+
const quote = await page().findByRole("button", {
|
|
184
|
+
name: "Ada Lovelace wrote:",
|
|
185
|
+
});
|
|
186
|
+
await expect(quote).toHaveAttribute("aria-expanded", "false");
|
|
187
|
+
await userEvent.click(quote);
|
|
188
|
+
await expect(await page().findByText(/Forwarded message/)).toBeVisible();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const ReplyOverALongMessage: Story = {
|
|
193
|
+
args: { url: `${messageUrl("billing")}/reply` },
|
|
194
|
+
play: async () => {
|
|
195
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
196
|
+
await waitFor(() => expect(subject).toHaveValue(`Re: ${LONG_SUBJECT}`));
|
|
197
|
+
const messages = page().getByTestId("conversation-messages");
|
|
198
|
+
await composeFirst(subject, messages);
|
|
199
|
+
await expect(
|
|
200
|
+
await within(messages).findByText(/Point 14\./),
|
|
201
|
+
).toBeInTheDocument();
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const ReplyOverAThread: Story = {
|
|
206
|
+
args: { url: `${threadUrl("venue", "venue-3")}/reply` },
|
|
207
|
+
play: async () => {
|
|
208
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
209
|
+
await waitFor(() => expect(subject).toHaveValue(`Re: ${TURNS_SUBJECT}`));
|
|
210
|
+
const messages = page().getByTestId("conversation-messages");
|
|
211
|
+
await composeFirst(subject, messages);
|
|
212
|
+
const newest = await within(messages).findByText(/Shall I book it/);
|
|
213
|
+
const oldest = await within(messages).findByText(/Two options/);
|
|
214
|
+
await composeFirst(newest, oldest);
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const PhoneLongReplyKeepsSendInReach: Story = {
|
|
219
|
+
globals: phone,
|
|
220
|
+
args: { url: `${messageUrl("lunch")}/reply/out-long-reply` },
|
|
221
|
+
play: async () => {
|
|
222
|
+
const editable = await writingSurface("Point 24.");
|
|
223
|
+
await waitFor(() =>
|
|
224
|
+
expect(editable.getBoundingClientRect().height).toBeGreaterThan(
|
|
225
|
+
window.innerHeight,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
const send = page().getByRole("button", { name: "Send" });
|
|
229
|
+
const button = send.getBoundingClientRect();
|
|
230
|
+
await expect(button.top).toBeGreaterThanOrEqual(0);
|
|
231
|
+
await expect(button.bottom).toBeLessThanOrEqual(window.innerHeight);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const PhoneSheet: Story = {
|
|
236
|
+
globals: phone,
|
|
237
|
+
play: async () => {
|
|
238
|
+
const sheet = await page().findByRole("dialog");
|
|
239
|
+
await expect(within(sheet).getByText("New Message")).toBeVisible();
|
|
240
|
+
await expect(within(sheet).getByLabelText("To:")).toBeVisible();
|
|
241
|
+
await expect(
|
|
242
|
+
within(sheet).getByRole("button", { name: "Send" }),
|
|
243
|
+
).toBeVisible();
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const PhoneKeyboardUp: Story = {
|
|
248
|
+
globals: phone,
|
|
249
|
+
beforeEach: () => {
|
|
250
|
+
const viewport = window.visualViewport;
|
|
251
|
+
if (!viewport) return;
|
|
252
|
+
Object.defineProperty(viewport, "height", {
|
|
253
|
+
configurable: true,
|
|
254
|
+
get: () => window.innerHeight - 320,
|
|
255
|
+
});
|
|
256
|
+
viewport.dispatchEvent(new Event("resize"));
|
|
257
|
+
return () => {
|
|
258
|
+
Reflect.deleteProperty(viewport, "height");
|
|
259
|
+
viewport.dispatchEvent(new Event("resize"));
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
play: async () => {
|
|
263
|
+
const expand = await page().findByRole("button", {
|
|
264
|
+
name: "Show recipients and subject",
|
|
265
|
+
});
|
|
266
|
+
await userEvent.click(expand);
|
|
267
|
+
const to = page().getByLabelText("To:");
|
|
268
|
+
await expect(to).toBeVisible();
|
|
269
|
+
await userEvent.type(to, "grace@example.org");
|
|
270
|
+
await expect(page().getByLabelText("To:")).toHaveValue("grace@example.org");
|
|
271
|
+
await expect(
|
|
272
|
+
page().queryByRole("button", { name: "Show recipients and subject" }),
|
|
273
|
+
).toBeNull();
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const PlainText: Story = {
|
|
278
|
+
args: { url: `/mail/${INBOX}/compose/out-plain` },
|
|
279
|
+
play: async () => {
|
|
280
|
+
const editor = await page().findByTestId("compose-body-plain");
|
|
281
|
+
await expect(editor.tagName).toBe("TEXTAREA");
|
|
282
|
+
await waitFor(() => expect(editor).toHaveValue(PLAIN_DRAFT));
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const SaveFailed: Story = {
|
|
287
|
+
parameters: {
|
|
288
|
+
msw: { handlers: mailHandlers(world, { draftSave: "refused" }) },
|
|
289
|
+
},
|
|
290
|
+
play: async () => {
|
|
291
|
+
await sendFromWork();
|
|
292
|
+
await userEvent.type(page().getByLabelText("To:"), "ada@acme.example,");
|
|
293
|
+
await expect(
|
|
294
|
+
await page().findByText("Save failed", {}, { timeout: 5000 }),
|
|
295
|
+
).toBeVisible();
|
|
296
|
+
await expect(await page().findByText("Couldn't save draft")).toBeVisible();
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const Sending: Story = {
|
|
301
|
+
args: { url: `/mail/${INBOX}/compose/out-misspelt` },
|
|
302
|
+
parameters: {
|
|
303
|
+
msw: { handlers: mailHandlers(world, { send: "holds" }) },
|
|
304
|
+
},
|
|
305
|
+
play: async () => {
|
|
306
|
+
await expect(
|
|
307
|
+
await page().findByRole("button", { name: "Remove ada@acme.example" }),
|
|
308
|
+
).toBeVisible();
|
|
309
|
+
const send = page().getByRole("button", { name: "Send" });
|
|
310
|
+
await userEvent.click(send);
|
|
311
|
+
await waitFor(() => expect(send).toHaveAttribute("aria-busy", "true"));
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const SendUnavailable: Story = {
|
|
316
|
+
parameters: {
|
|
317
|
+
msw: { handlers: mailHandlers(withoutSmtp(world)) },
|
|
318
|
+
},
|
|
319
|
+
play: async () => {
|
|
320
|
+
await sendFromWork();
|
|
321
|
+
await expect(
|
|
322
|
+
await page().findByTestId("compose-smtp-missing-banner"),
|
|
323
|
+
).toBeVisible();
|
|
324
|
+
await userEvent.type(page().getByLabelText("To:"), "ada@acme.example,");
|
|
325
|
+
await userEvent.click(page().getByRole("button", { name: "Send" }));
|
|
326
|
+
await refusal(SMTP_MISSING_MESSAGE);
|
|
327
|
+
await expect(page().getByLabelText("To:")).toBeVisible();
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export const ChooseAnAccount: Story = {
|
|
332
|
+
play: async () => {
|
|
333
|
+
const from = await page().findByLabelText("From:");
|
|
334
|
+
await expect(from).toHaveValue("");
|
|
335
|
+
await userEvent.click(page().getByRole("button", { name: "Send" }));
|
|
336
|
+
await refusal("Choose an account to send from.");
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const SendWithNoRecipient: Story = {
|
|
341
|
+
play: async () => {
|
|
342
|
+
await sendFromWork();
|
|
343
|
+
await userEvent.click(page().getByRole("button", { name: "Send" }));
|
|
344
|
+
await refusal("Add a To address before sending.");
|
|
345
|
+
await expect(page().getByLabelText("To:")).toBeVisible();
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const NothingToForward: Story = {
|
|
350
|
+
args: { url: `${messageUrl("scans")}/forward` },
|
|
351
|
+
play: async () => {
|
|
352
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
353
|
+
await waitFor(() => expect(subject).toHaveValue(`Fwd: ${SCANS_SUBJECT}`));
|
|
354
|
+
await expect(
|
|
355
|
+
await page().findByTestId("compose-quote-missing"),
|
|
356
|
+
).toBeVisible();
|
|
357
|
+
await userEvent.type(page().getByLabelText("To:"), "ada@acme.example,");
|
|
358
|
+
await userEvent.click(page().getByRole("button", { name: "Send" }));
|
|
359
|
+
await refusal(NO_QUOTABLE_BODY_FORWARD_MESSAGE);
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const ReplyWithNothingToQuote: Story = {
|
|
364
|
+
args: { url: `${messageUrl("scans")}/reply` },
|
|
365
|
+
play: async () => {
|
|
366
|
+
const subject = await page().findByPlaceholderText("Subject");
|
|
367
|
+
await waitFor(() => expect(subject).toHaveValue(`Re: ${SCANS_SUBJECT}`));
|
|
368
|
+
const banner = await page().findByTestId("compose-quote-missing");
|
|
369
|
+
await expect(banner).toBeVisible();
|
|
370
|
+
await expect(
|
|
371
|
+
page().getByRole("button", { name: "Send" }),
|
|
372
|
+
).not.toHaveAttribute("title");
|
|
373
|
+
},
|
|
374
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ComposeFab as ComposeFabButton } from "@remit/ui";
|
|
2
2
|
import { useOpenCompose, useOpensDetail } from "@/routing";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -18,18 +18,5 @@ export const ComposeFab = () => {
|
|
|
18
18
|
|
|
19
19
|
if (opensDetail) return null;
|
|
20
20
|
|
|
21
|
-
return
|
|
22
|
-
<button
|
|
23
|
-
type="button"
|
|
24
|
-
onClick={compose}
|
|
25
|
-
aria-label="Compose new message"
|
|
26
|
-
className="lg:hidden fixed right-4 z-30 h-14 w-14 rounded-full bg-accent text-accent-fg shadow-lg flex items-center justify-center hover:opacity-90 active:scale-95 transition-all"
|
|
27
|
-
style={{
|
|
28
|
-
// 16px breathing room above the iOS home-indicator inset.
|
|
29
|
-
bottom: "calc(env(safe-area-inset-bottom, 0) + 1rem)",
|
|
30
|
-
}}
|
|
31
|
-
>
|
|
32
|
-
<Pencil className="size-6" />
|
|
33
|
-
</button>
|
|
34
|
-
);
|
|
21
|
+
return <ComposeFabButton onCompose={compose} />;
|
|
35
22
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
3
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
4
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
5
|
+
import { mailboxIdFor, mailWorld, WORK } from "@/mocks/story-frame/mail-world";
|
|
6
|
+
|
|
7
|
+
const world = mailWorld();
|
|
8
|
+
const inbox = `/mail/${mailboxIdFor(WORK, "Inbox")}`;
|
|
9
|
+
const openInInbox = `${inbox}/thread-q3-planning/msg-q3-planning`;
|
|
10
|
+
const desktop = { viewport: { value: "desktop", isRotated: false } };
|
|
11
|
+
const phone = { viewport: { value: "mobile", isRotated: false } };
|
|
12
|
+
|
|
13
|
+
const meta = {
|
|
14
|
+
title: "Playground/Shipped/Mail/Shell",
|
|
15
|
+
component: AppStory,
|
|
16
|
+
args: { url: "/mail/brief/thread-q3-planning/msg-q3-planning#intelligence" },
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: "fullscreen",
|
|
19
|
+
msw: { handlers: mailHandlers(world) },
|
|
20
|
+
},
|
|
21
|
+
globals: desktop,
|
|
22
|
+
} satisfies Meta<typeof AppStory>;
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
const page = () => within(document.body);
|
|
28
|
+
|
|
29
|
+
const rows = (): HTMLElement[] =>
|
|
30
|
+
Array.from(document.querySelectorAll<HTMLElement>("[data-list-row]"));
|
|
31
|
+
|
|
32
|
+
const conversation = () =>
|
|
33
|
+
page().findByRole("heading", { level: 1, name: "Q3 planning" });
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
play: async () => {
|
|
37
|
+
await expect(await conversation()).toBeVisible();
|
|
38
|
+
await expect(page().getByRole("button", { name: "Compose" })).toBeVisible();
|
|
39
|
+
await expect(page().getAllByLabelText("Search mail")).toHaveLength(1);
|
|
40
|
+
await waitFor(() => expect(rows().length).toBeGreaterThan(1));
|
|
41
|
+
await expect(await page().findByText("Intelligence")).toBeVisible();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Inbox: Story = {
|
|
46
|
+
args: { url: `${openInInbox}#intelligence` },
|
|
47
|
+
play: async () => {
|
|
48
|
+
await expect(await conversation()).toBeVisible();
|
|
49
|
+
await expect(await page().findByText("in:inbox")).toBeVisible();
|
|
50
|
+
await expect(page().getByLabelText("Expand filters")).toBeVisible();
|
|
51
|
+
await expect(await page().findByText("Intelligence")).toBeVisible();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const IntelligenceClosed: Story = {
|
|
56
|
+
play: async () => {
|
|
57
|
+
await expect(await conversation()).toBeVisible();
|
|
58
|
+
const hide = await page().findByRole("button", {
|
|
59
|
+
name: "Hide intelligence sidebar",
|
|
60
|
+
});
|
|
61
|
+
await userEvent.click(hide);
|
|
62
|
+
const show = await page().findByRole("button", {
|
|
63
|
+
name: "Show intelligence sidebar",
|
|
64
|
+
});
|
|
65
|
+
await expect(show).toBeEnabled();
|
|
66
|
+
await waitFor(() => expect(page().queryByText("Intelligence")).toBeNull());
|
|
67
|
+
await expect(await conversation()).toBeVisible();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const NoThreadOpen: Story = {
|
|
72
|
+
args: { url: "/mail/flagged" },
|
|
73
|
+
play: async () => {
|
|
74
|
+
await expect(await page().findByText("Invoice #1042")).toBeVisible();
|
|
75
|
+
await expect(page().getByText("Select a thread to read")).toBeVisible();
|
|
76
|
+
await expect(
|
|
77
|
+
page().getByRole("button", { name: "Show intelligence sidebar" }),
|
|
78
|
+
).toBeDisabled();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const TwoPane: Story = {
|
|
83
|
+
args: { url: `${openInInbox}#intelligence` },
|
|
84
|
+
globals: { viewport: { value: "twoPane", isRotated: false } },
|
|
85
|
+
play: async () => {
|
|
86
|
+
await expect(await conversation()).toBeVisible();
|
|
87
|
+
await expect(page().getByRole("button", { name: "Compose" })).toBeVisible();
|
|
88
|
+
await waitFor(() => expect(rows().length).toBeGreaterThan(1));
|
|
89
|
+
await expect(page().queryByText("Intelligence")).toBeNull();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const TabletSinglePane: Story = {
|
|
94
|
+
args: { url: inbox },
|
|
95
|
+
globals: { viewport: { value: "tablet", isRotated: false } },
|
|
96
|
+
play: async () => {
|
|
97
|
+
await expect(await page().findByText("Q3 planning")).toBeVisible();
|
|
98
|
+
await expect(page().queryByRole("button", { name: "Compose" })).toBeNull();
|
|
99
|
+
await expect(
|
|
100
|
+
page().getByRole("button", { name: "Compose new message" }),
|
|
101
|
+
).toBeVisible();
|
|
102
|
+
await expect(page().getByRole("button", { name: "Search" })).toBeVisible();
|
|
103
|
+
await expect(page().getByRole("button", { name: "Menu" })).toBeVisible();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const Phone: Story = {
|
|
108
|
+
args: { url: inbox },
|
|
109
|
+
globals: phone,
|
|
110
|
+
play: async () => {
|
|
111
|
+
await expect(await page().findByText("Q3 planning")).toBeVisible();
|
|
112
|
+
await expect(
|
|
113
|
+
page().getByRole("button", { name: "Compose new message" }),
|
|
114
|
+
).toBeVisible();
|
|
115
|
+
await expect(page().queryByText("Starred")).toBeNull();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const PhoneNavSlideOver: Story = {
|
|
120
|
+
args: { url: inbox },
|
|
121
|
+
globals: phone,
|
|
122
|
+
play: async () => {
|
|
123
|
+
await page().findByText("Q3 planning");
|
|
124
|
+
await userEvent.click(page().getByRole("button", { name: "Menu" }));
|
|
125
|
+
await expect(await page().findByText("Starred")).toBeVisible();
|
|
126
|
+
await expect(page().getByText("Daily brief")).toBeVisible();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const Loading: Story = {
|
|
131
|
+
args: { url: "/mail/brief" },
|
|
132
|
+
parameters: {
|
|
133
|
+
msw: { handlers: mailHandlers(world, { config: "holds" }) },
|
|
134
|
+
},
|
|
135
|
+
play: async () => {
|
|
136
|
+
await waitFor(() =>
|
|
137
|
+
expect(document.querySelector(".animate-pulse")).not.toBeNull(),
|
|
138
|
+
);
|
|
139
|
+
await expect(rows()).toHaveLength(0);
|
|
140
|
+
await expect(page().queryByRole("button", { name: "Compose" })).toBeNull();
|
|
141
|
+
},
|
|
142
|
+
};
|