@remit/web-client 0.0.321 → 0.0.323
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 +4 -4
- package/src/auth/SignInAgain.tsx +18 -0
- package/src/auth/amplify-config.test.ts +1 -107
- package/src/auth/auth-interceptor.test.ts +0 -6
- package/src/auth/cognito/cognito-token.test.ts +2 -4
- package/src/components/calendar/AgendaStrip.tsx +1 -1
- package/src/components/calendar/CalendarWorkspace.tsx +1 -1
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
- package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
- package/src/components/compose/ComposeForm.tsx +11 -6
- package/src/components/compose/FullCompose.tsx +1 -1
- package/src/components/compose/MobileComposeSheet.tsx +1 -2
- package/src/components/compose/compose.stories.tsx +18 -0
- package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
- package/src/components/config-import/config-import.fixtures.ts +8 -9
- package/src/components/config-import/config-import.stories.tsx +2 -2
- package/src/components/layout/MailTopBar.tsx +3 -0
- package/src/components/mail/BriefPane.tsx +0 -3
- package/src/components/mail/ConversationView.tsx +1 -3
- package/src/components/mail/DailyBrief.tsx +5 -28
- package/src/components/mail/FlaggedList.tsx +3 -4
- package/src/components/mail/FlaggedPane.tsx +0 -3
- package/src/components/mail/IntelligenceDrawer.tsx +0 -3
- package/src/components/mail/IntelligencePane.render.test.ts +0 -1
- package/src/components/mail/IntelligencePane.test.ts +1 -95
- package/src/components/mail/IntelligencePane.tsx +3 -93
- package/src/components/mail/MailListHeader.tsx +12 -43
- package/src/components/mail/MailNav.tsx +6 -2
- package/src/components/mail/MailViewChrome.tsx +1 -11
- package/src/components/mail/MailboxPane.tsx +4 -34
- package/src/components/mail/MessageActionMenu.tsx +6 -6
- package/src/components/mail/MessageAttachments.tsx +35 -0
- package/src/components/mail/MessageBody.tsx +1 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
- package/src/components/mail/MessageCard.tsx +16 -4
- package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
- package/src/components/mail/MessageList.tsx +23 -11
- package/src/components/mail/MessageRow.render.test.ts +0 -7
- package/src/components/mail/MoveToTrigger.tsx +1 -1
- package/src/components/mail/RawMessageView.tsx +1 -1
- package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
- package/src/components/mail/SelectionWizardHost.tsx +16 -102
- package/src/components/mail/authenticity-warning.stories.tsx +0 -21
- package/src/components/mail/daily-brief.stories.tsx +31 -17
- package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
- package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
- package/src/components/mail/message-gone.stories.tsx +44 -0
- package/src/components/mail/offer-times-reply.render.test.ts +25 -1
- package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
- package/src/components/overlay-escape.render.test.ts +7 -3
- package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
- package/src/components/settings/AccountFormPanel.test.ts +1 -36
- package/src/components/settings/AccountFormPanel.tsx +5 -25
- package/src/components/settings/AccountServices.tsx +1 -1
- package/src/components/settings/AdvancedNavIcon.tsx +3 -3
- package/src/components/settings/CalendarFeedCard.tsx +2 -1
- package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
- package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
- package/src/components/settings/FilterActionError.tsx +1 -1
- package/src/components/settings/FilterEditor.render.test.ts +3 -27
- package/src/components/settings/FilterEditor.tsx +4 -24
- package/src/components/settings/FilterEditorSurface.tsx +0 -3
- package/src/components/settings/FiltersList.render.test.ts +5 -24
- package/src/components/settings/FiltersList.stories.tsx +0 -1
- package/src/components/settings/FiltersList.tsx +10 -35
- package/src/components/settings/WarningsNavIcon.tsx +12 -0
- package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
- package/src/components/settings/WarningsPanel.render.test.ts +56 -0
- package/src/components/settings/WarningsPanel.tsx +244 -0
- package/src/components/settings/accounts.stories.tsx +18 -0
- package/src/components/settings/advanced.stories.tsx +3 -125
- package/src/components/settings/settings-filter.stories.tsx +2 -42
- package/src/components/settings/warnings.stories.tsx +290 -0
- package/src/components/ui/ErrorBannerProvider.tsx +5 -3
- package/src/components/ui/error-banners.test.ts +1 -22
- package/src/components/ui/error-banners.ts +9 -20
- package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
- package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
- package/src/hooks/calendar/useCalendarWrites.ts +2 -1
- package/src/hooks/intelligence-calendar.render.test.ts +0 -1
- package/src/hooks/useDeleteMessages.test.ts +1 -42
- package/src/hooks/useFilters.test.ts +0 -8
- package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
- package/src/hooks/useIntelligenceData.test.ts +9 -47
- package/src/hooks/useIntelligenceData.ts +7 -100
- package/src/hooks/useLayoutTier.test.ts +0 -48
- package/src/hooks/useReportSpam.ts +1 -1
- package/src/hooks/useRescueCandidates.test.ts +0 -23
- package/src/hooks/useRulePreview.ts +8 -7
- package/src/hooks/useSearchTokenContext.ts +2 -3
- package/src/hooks/useStaleAccountSync.test.ts +0 -17
- package/src/hooks/useStarredThreads.test.ts +0 -10
- package/src/hooks/useTriggerSync.test.ts +0 -6
- package/src/hooks/useVisualViewport.render.test.ts +63 -0
- package/src/hooks/useWarningsAttention.ts +37 -0
- package/src/lib/account-order.test.ts +0 -10
- package/src/lib/adjacent-message.test.ts +0 -9
- package/src/lib/app-info.ts +0 -3
- package/src/lib/autodiscovery.test.ts +0 -47
- package/src/lib/bug-report.test.ts +38 -0
- package/src/lib/bug-report.ts +10 -7
- package/src/lib/bulk-action-copy.ts +1 -1
- package/src/lib/calendar-route.test.ts +0 -5
- package/src/lib/config-import.test.ts +3 -39
- package/src/lib/dedupe-thread-messages.test.ts +0 -4
- package/src/lib/desktop-breakpoint.test.ts +0 -8
- package/src/lib/drafts.test.ts +0 -23
- package/src/lib/fatal-error.test.ts +0 -19
- package/src/lib/folder-roles.test.ts +0 -4
- package/src/lib/hot-sync-window.test.ts +0 -5
- package/src/lib/list-header-chrome.ts +1 -1
- package/src/lib/mail-context.ts +1 -1
- package/src/lib/message-attachments.test.ts +77 -0
- package/src/lib/message-attachments.ts +31 -15
- package/src/lib/message-body-source.test.ts +21 -4
- package/src/lib/message-body-source.ts +5 -2
- package/src/lib/onboarding-completion.test.ts +0 -16
- package/src/lib/organize/filter-edit-model.test.ts +2 -32
- package/src/lib/organize/filter-edit-model.ts +6 -16
- package/src/lib/organize/organize-model.test.ts +7 -40
- package/src/lib/organize/organize-model.ts +16 -18
- package/src/lib/organize/rule-model.test.ts +24 -202
- package/src/lib/organize/rule-model.ts +19 -166
- package/src/lib/organize/search-to-rule.test.ts +2 -36
- package/src/lib/organize/search-to-rule.ts +1 -16
- package/src/lib/provider-presets.test.ts +1 -77
- package/src/lib/query-error-handler.test.ts +0 -61
- package/src/lib/query-escalation.integration.test.ts +4 -4
- package/src/lib/result-count.ts +1 -1
- package/src/lib/result-folder.ts +1 -1
- package/src/lib/rum-adapter.test.ts +0 -104
- package/src/lib/search-query.test.ts +0 -11
- package/src/lib/search-result.test.ts +2 -142
- package/src/lib/search-result.ts +3 -73
- package/src/lib/search-scope.test.ts +1 -92
- package/src/lib/search-scope.ts +0 -29
- package/src/lib/search-surface.ts +2 -2
- package/src/lib/search-token-index.test.ts +0 -8
- package/src/lib/search-tokens.ts +15 -16
- package/src/lib/starred-rows.test.ts +0 -4
- package/src/lib/sync-warning-entries.ts +33 -0
- package/src/lib/wizard-selection.ts +1 -1
- package/src/mocks/story-frame/mail-handlers.ts +12 -8
- package/src/mocks/story-frame/mail-world.ts +5 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/calendar/$view.$date/index.tsx +1 -1
- package/src/routes/calendar.tsx +2 -1
- package/src/routes/mail/brief.tsx +1 -1
- package/src/routes/mail/flagged.tsx +1 -1
- package/src/routes/mail.tsx +5 -3
- package/src/routes/settings/-accounts.test.ts +0 -5
- package/src/routes/settings/-filters.test.ts +0 -1
- package/src/routes/settings/-shared/help-copy.tsx +15 -0
- package/src/routes/settings/accounts.tsx +1 -1
- package/src/routes/settings/advanced.tsx +1 -4
- package/src/routes/settings/filters.tsx +1 -2
- package/src/routes/settings/folders.tsx +1 -1
- package/src/routes/settings/labels.tsx +1 -1
- package/src/routes/settings/senders.tsx +1 -1
- package/src/routes/settings/warnings.tsx +36 -0
- package/src/routes/settings.tsx +7 -0
- package/src/routing/wizard-history.test.ts +1 -45
- package/src/runtime-config.ts +5 -0
- package/src/test-support/fixtures.ts +1 -1
- package/src/components/compose/draft-discard-banner.test.ts +0 -76
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
- package/src/components/mail/SpamRescue.render.test.ts +0 -35
- package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
- package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
- package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
- package/src/components/settings/QuarantinePanel.tsx +0 -178
- package/src/components/ui/DropdownMenu.tsx +0 -99
- package/src/components/ui/EmptyState.tsx +0 -11
- package/src/components/ui/ErrorBanner.render.test.ts +0 -104
- package/src/components/ui/ErrorBanner.stories.tsx +0 -132
- package/src/components/ui/ErrorBanner.tsx +0 -106
- package/src/components/ui/ErrorBannerStack.tsx +0 -35
- package/src/components/ui/ErrorState.render.test.ts +0 -82
- package/src/components/ui/ErrorState.tsx +0 -80
- package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
- package/src/hooks/useMoveMessages.test.ts +0 -63
- package/src/hooks/useOrganizeWiden.test.ts +0 -217
- package/src/hooks/useOrganizeWiden.ts +0 -112
- package/src/hooks/useSemanticSearch.ts +0 -132
- package/src/hooks/useSemanticSearchEnabled.ts +0 -23
- package/src/hooks/useVisualViewport.test.ts +0 -126
- package/src/lib/organize/sender-fallback.test.ts +0 -52
- package/src/lib/organize/sender-fallback.ts +0 -38
- package/src/lib/rescue-telemetry.test.ts +0 -75
- package/src/router.test.ts +0 -81
|
@@ -10,9 +10,7 @@ import { FilterEditor } from "./FilterEditor";
|
|
|
10
10
|
* `Flows/Settings Filters` — editing a standing filter in the same chip editor
|
|
11
11
|
* the Organize surface uses (RFC 038 D6), rendered from the real web-client
|
|
12
12
|
* component the settings page mounts. Each story opens a persisted filter into
|
|
13
|
-
* the editor: a literal rule
|
|
14
|
-
* until-a-date rule, and the degraded case where the deployment cannot evaluate
|
|
15
|
-
* the anchor and the widen lists inactive (D4).
|
|
13
|
+
* the editor: a literal rule and an until-a-date rule.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
16
|
const ACCOUNT_ID = "acc-1";
|
|
@@ -47,7 +45,6 @@ const makeFilter = (
|
|
|
47
45
|
scope: "Standing",
|
|
48
46
|
state: "Active",
|
|
49
47
|
disabledReason: "None",
|
|
50
|
-
hasAnchor: false,
|
|
51
48
|
ruleChangedAt: 0,
|
|
52
49
|
actionChangedAt: 0,
|
|
53
50
|
matchOperator: "And",
|
|
@@ -65,7 +62,7 @@ const makeFilter = (
|
|
|
65
62
|
/**
|
|
66
63
|
* A bare client with no backend. The editor opens on `loading` and its preview
|
|
67
64
|
* settles to the count region's error state — these stories exercise the editor
|
|
68
|
-
* chrome (chips, read-only lifecycle
|
|
65
|
+
* chrome (chips, read-only lifecycle), not the live count.
|
|
69
66
|
*/
|
|
70
67
|
function seededClient(): QueryClient {
|
|
71
68
|
return new QueryClient({
|
|
@@ -127,22 +124,6 @@ export const EditStandingRule: Story = {
|
|
|
127
124
|
),
|
|
128
125
|
};
|
|
129
126
|
|
|
130
|
-
/** A filter with a semantic anchor — the widen lists as an active chip. */
|
|
131
|
-
export const EditAnchoredRule: Story = {
|
|
132
|
-
name: "Edit Anchored Rule",
|
|
133
|
-
render: () => (
|
|
134
|
-
<SheetStage>
|
|
135
|
-
<FilterEditor
|
|
136
|
-
accountId={ACCOUNT_ID}
|
|
137
|
-
filter={makeFilter({ hasAnchor: true, name: "GitHub" })}
|
|
138
|
-
folders={FOLDERS}
|
|
139
|
-
labels={LABELS}
|
|
140
|
-
onClose={() => undefined}
|
|
141
|
-
/>
|
|
142
|
-
</SheetStage>
|
|
143
|
-
),
|
|
144
|
-
};
|
|
145
|
-
|
|
146
127
|
/** A temporary filter — the scope loads as until-a-date with the stored date. */
|
|
147
128
|
export const EditUntilADate: Story = {
|
|
148
129
|
name: "Edit Until A Date",
|
|
@@ -162,24 +143,3 @@ export const EditUntilADate: Story = {
|
|
|
162
143
|
</SheetStage>
|
|
163
144
|
),
|
|
164
145
|
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The deployment cannot evaluate the filter's anchor (RFC 038 D4): the widen
|
|
168
|
-
* chip lists inactive, the rule matches by its literal clauses only, and the
|
|
169
|
-
* dead anchor is removable.
|
|
170
|
-
*/
|
|
171
|
-
export const DegradedAnchor: Story = {
|
|
172
|
-
name: "Degraded Anchor",
|
|
173
|
-
render: () => (
|
|
174
|
-
<SheetStage>
|
|
175
|
-
<FilterEditor
|
|
176
|
-
accountId={ACCOUNT_ID}
|
|
177
|
-
filter={makeFilter({ hasAnchor: true, name: "Newsletters" })}
|
|
178
|
-
folders={FOLDERS}
|
|
179
|
-
labels={LABELS}
|
|
180
|
-
semanticUnavailable
|
|
181
|
-
onClose={() => undefined}
|
|
182
|
-
/>
|
|
183
|
-
</SheetStage>
|
|
184
|
-
),
|
|
185
|
-
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapQuarantineResponse,
|
|
3
|
+
RemitImapSyncWarningResponse,
|
|
4
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
6
|
+
import { HttpResponse, http } from "msw";
|
|
7
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
8
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
9
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
10
|
+
import {
|
|
11
|
+
MINUTE,
|
|
12
|
+
mailboxIdFor,
|
|
13
|
+
mailWorld,
|
|
14
|
+
NOW,
|
|
15
|
+
PERSONAL,
|
|
16
|
+
} from "@/mocks/story-frame/mail-world";
|
|
17
|
+
|
|
18
|
+
const setAside = (
|
|
19
|
+
id: string,
|
|
20
|
+
role: "Inbox" | "Archive",
|
|
21
|
+
failureCode: RemitImapQuarantineResponse["failureCode"],
|
|
22
|
+
failureMessage: string,
|
|
23
|
+
): RemitImapQuarantineResponse => ({
|
|
24
|
+
quarantineId: `q-${id}`,
|
|
25
|
+
accountConfigId: "cfg-1",
|
|
26
|
+
accountId: PERSONAL,
|
|
27
|
+
mailboxId: mailboxIdFor(PERSONAL, role),
|
|
28
|
+
uidValidity: 1_712_000_000,
|
|
29
|
+
uid: 40_217,
|
|
30
|
+
mailboxRole: role,
|
|
31
|
+
mailboxPath: role === "Inbox" ? "INBOX" : "Archive",
|
|
32
|
+
quarantinedAt: NOW,
|
|
33
|
+
attempts: 3,
|
|
34
|
+
failureStage: "BodyParse",
|
|
35
|
+
failureCode,
|
|
36
|
+
failureMessage,
|
|
37
|
+
workerVersion: "worker 1.0.0",
|
|
38
|
+
contentType: "multipart/mixed",
|
|
39
|
+
sizeBytes: 184_233,
|
|
40
|
+
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
41
|
+
createdAt: NOW,
|
|
42
|
+
updatedAt: NOW,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const failing = (
|
|
46
|
+
uid: number,
|
|
47
|
+
role: "Inbox" | "Archive",
|
|
48
|
+
stage: RemitImapSyncWarningResponse["stage"],
|
|
49
|
+
attempts: number,
|
|
50
|
+
): RemitImapSyncWarningResponse => ({
|
|
51
|
+
syncWarningId: `w-${uid}`,
|
|
52
|
+
accountConfigId: "cfg-1",
|
|
53
|
+
accountId: PERSONAL,
|
|
54
|
+
mailboxId: mailboxIdFor(PERSONAL, role),
|
|
55
|
+
uidValidity: 1_712_000_000,
|
|
56
|
+
uid,
|
|
57
|
+
stage,
|
|
58
|
+
attempts,
|
|
59
|
+
createdAt: NOW - uid * MINUTE,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const unreadableBody = setAside(
|
|
63
|
+
"1",
|
|
64
|
+
"Inbox",
|
|
65
|
+
"UnreadableBody",
|
|
66
|
+
"multipart boundary was never closed",
|
|
67
|
+
);
|
|
68
|
+
const unknownCharset = setAside(
|
|
69
|
+
"2",
|
|
70
|
+
"Archive",
|
|
71
|
+
"UnknownCharset",
|
|
72
|
+
"declared charset is not a known encoding",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const bodyGaveUp = failing(3, "Inbox", "Body", 3);
|
|
76
|
+
const headerRetrying = failing(5, "Archive", "Header", 6);
|
|
77
|
+
|
|
78
|
+
const meta = {
|
|
79
|
+
title: "Playground/Shipped/Settings/Warnings",
|
|
80
|
+
component: AppStory,
|
|
81
|
+
args: { url: "/settings/warnings" },
|
|
82
|
+
parameters: {
|
|
83
|
+
layout: "fullscreen",
|
|
84
|
+
msw: { handlers: mailHandlers(mailWorld()) },
|
|
85
|
+
},
|
|
86
|
+
} satisfies Meta<typeof AppStory>;
|
|
87
|
+
export default meta;
|
|
88
|
+
|
|
89
|
+
type Story = StoryObj<typeof meta>;
|
|
90
|
+
|
|
91
|
+
export const Empty: Story = {
|
|
92
|
+
play: async ({ canvasElement }) => {
|
|
93
|
+
const canvas = within(canvasElement);
|
|
94
|
+
await expect(
|
|
95
|
+
await canvas.findByText(/Every message is syncing/),
|
|
96
|
+
).toBeVisible();
|
|
97
|
+
await expect(
|
|
98
|
+
canvas.getByText(/Every message has been read successfully/),
|
|
99
|
+
).toBeVisible();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Mixed: Story = {
|
|
104
|
+
parameters: {
|
|
105
|
+
msw: {
|
|
106
|
+
handlers: mailHandlers(
|
|
107
|
+
mailWorld({
|
|
108
|
+
syncWarnings: [bodyGaveUp, headerRetrying],
|
|
109
|
+
syncWarningsTotal: 2,
|
|
110
|
+
quarantine: [unreadableBody],
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
play: async ({ canvasElement }) => {
|
|
116
|
+
const canvas = within(canvasElement);
|
|
117
|
+
await expect(await canvas.findByText("Gave up")).toBeVisible();
|
|
118
|
+
await expect(canvas.getByText("Retrying")).toBeVisible();
|
|
119
|
+
await expect(
|
|
120
|
+
canvas.getByText(/multipart boundary was never closed/),
|
|
121
|
+
).toBeVisible();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const OverCap: Story = {
|
|
126
|
+
parameters: {
|
|
127
|
+
msw: {
|
|
128
|
+
handlers: mailHandlers(
|
|
129
|
+
mailWorld({
|
|
130
|
+
syncWarnings: [bodyGaveUp, headerRetrying],
|
|
131
|
+
syncWarningsTotal: 4_812,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
play: async ({ canvasElement }) => {
|
|
137
|
+
const canvas = within(canvasElement);
|
|
138
|
+
await expect(
|
|
139
|
+
await canvas.findByText(/Showing the 2 newest of 4812/),
|
|
140
|
+
).toBeVisible();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const QuarantineAlert: Story = {
|
|
145
|
+
parameters: {
|
|
146
|
+
msw: {
|
|
147
|
+
handlers: mailHandlers(
|
|
148
|
+
mailWorld({ quarantine: [unreadableBody, unknownCharset] }),
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
play: async ({ canvasElement }) => {
|
|
153
|
+
const canvas = within(canvasElement);
|
|
154
|
+
await expect(
|
|
155
|
+
await canvas.findByText("2 messages could not be read."),
|
|
156
|
+
).toBeVisible();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const Loading: Story = {
|
|
161
|
+
parameters: {
|
|
162
|
+
msw: {
|
|
163
|
+
handlers: [
|
|
164
|
+
http.get(
|
|
165
|
+
"/api/me/quarantine",
|
|
166
|
+
() => new Promise<never>(() => undefined),
|
|
167
|
+
),
|
|
168
|
+
...mailHandlers(mailWorld()),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
play: async ({ canvasElement }) => {
|
|
173
|
+
const canvas = within(canvasElement);
|
|
174
|
+
await expect(
|
|
175
|
+
await canvas.findByText("Checking for warnings…"),
|
|
176
|
+
).toBeVisible();
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const ReadFailure: Story = {
|
|
181
|
+
parameters: {
|
|
182
|
+
msw: {
|
|
183
|
+
handlers: [
|
|
184
|
+
http.get("/api/me/quarantine", () => HttpResponse.error()),
|
|
185
|
+
...mailHandlers(mailWorld()),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
play: async ({ canvasElement }) => {
|
|
190
|
+
const canvas = within(canvasElement);
|
|
191
|
+
await expect(
|
|
192
|
+
await canvas.findByText("The warnings could not be read."),
|
|
193
|
+
).toBeVisible();
|
|
194
|
+
await expect(
|
|
195
|
+
canvas.getByRole("button", { name: "Report this" }),
|
|
196
|
+
).toBeVisible();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const SignedOut: Story = {
|
|
201
|
+
parameters: {
|
|
202
|
+
msw: {
|
|
203
|
+
handlers: [
|
|
204
|
+
http.get("/api/me/quarantine", () =>
|
|
205
|
+
HttpResponse.json(
|
|
206
|
+
{ status: 401, message: "Unauthorized" },
|
|
207
|
+
{ status: 401 },
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
...mailHandlers(mailWorld()),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
play: async ({ canvasElement }) => {
|
|
215
|
+
const canvas = within(canvasElement);
|
|
216
|
+
await expect(
|
|
217
|
+
await canvas.findByText("Your session has ended."),
|
|
218
|
+
).toBeVisible();
|
|
219
|
+
await expect(canvas.queryByText(/Every message is syncing/)).toBeNull();
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const failingWorld = () =>
|
|
224
|
+
mailHandlers(mailWorld({ syncWarnings: [bodyGaveUp], syncWarningsTotal: 1 }));
|
|
225
|
+
|
|
226
|
+
export const NavDot: Story = {
|
|
227
|
+
parameters: { msw: { handlers: failingWorld() } },
|
|
228
|
+
play: async ({ canvasElement }) => {
|
|
229
|
+
const canvas = within(canvasElement);
|
|
230
|
+
await expect(await canvas.findByText("Sync warnings")).toBeInTheDocument();
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const DotOnMail: Story = {
|
|
235
|
+
args: { url: "/mail/flagged" },
|
|
236
|
+
parameters: { msw: { handlers: failingWorld() } },
|
|
237
|
+
play: async ({ canvasElement }) => {
|
|
238
|
+
const canvas = within(canvasElement);
|
|
239
|
+
await expect(
|
|
240
|
+
await canvas.findByRole("button", { name: "Settings — Sync warnings" }),
|
|
241
|
+
).toBeVisible();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const NoDotOnMail: Story = {
|
|
246
|
+
args: { url: "/mail/flagged" },
|
|
247
|
+
play: async ({ canvasElement }) => {
|
|
248
|
+
const canvas = within(canvasElement);
|
|
249
|
+
await expect(
|
|
250
|
+
await canvas.findByRole("button", { name: "Settings" }),
|
|
251
|
+
).toBeVisible();
|
|
252
|
+
await expect(canvas.queryByText("Sync warnings")).toBeNull();
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const DotOnPhone: Story = {
|
|
257
|
+
args: { url: "/mail/flagged" },
|
|
258
|
+
globals: { viewport: { value: "mobile", isRotated: false } },
|
|
259
|
+
parameters: { msw: { handlers: failingWorld() } },
|
|
260
|
+
play: async ({ canvasElement }) => {
|
|
261
|
+
const canvas = within(canvasElement);
|
|
262
|
+
const [menu] = await canvas.findAllByRole("button", { name: "Menu" });
|
|
263
|
+
await userEvent.click(menu);
|
|
264
|
+
const page = within(canvasElement.ownerDocument.body);
|
|
265
|
+
await expect(await page.findByText("Sync warnings")).toBeInTheDocument();
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const DotOnSignedOut: Story = {
|
|
270
|
+
args: { url: "/mail/flagged" },
|
|
271
|
+
parameters: {
|
|
272
|
+
msw: {
|
|
273
|
+
handlers: [
|
|
274
|
+
http.get("/api/me/quarantine", () =>
|
|
275
|
+
HttpResponse.json(
|
|
276
|
+
{ status: 401, message: "Unauthorized" },
|
|
277
|
+
{ status: 401 },
|
|
278
|
+
),
|
|
279
|
+
),
|
|
280
|
+
...mailHandlers(mailWorld()),
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
play: async ({ canvasElement }) => {
|
|
285
|
+
const canvas = within(canvasElement);
|
|
286
|
+
await expect(
|
|
287
|
+
await canvas.findByRole("button", { name: "Settings — Session ended" }),
|
|
288
|
+
).toBeVisible();
|
|
289
|
+
},
|
|
290
|
+
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ErrorBannerAction,
|
|
3
|
+
type ErrorBannerEntry,
|
|
4
|
+
ErrorBannerStack,
|
|
5
|
+
} from "@remit/ui";
|
|
1
6
|
import {
|
|
2
7
|
createContext,
|
|
3
8
|
type ReactNode,
|
|
@@ -9,13 +14,10 @@ import {
|
|
|
9
14
|
import { buildBugReportContext, buildGitHubIssueUrl } from "@/lib/bug-report";
|
|
10
15
|
import { isAlwaysFatal } from "@/lib/error-classifier";
|
|
11
16
|
import { reportFatalError } from "@/lib/fatal-error";
|
|
12
|
-
import { ErrorBannerStack } from "./ErrorBannerStack.js";
|
|
13
17
|
import {
|
|
14
18
|
appendBanner,
|
|
15
19
|
buildEntry,
|
|
16
20
|
dismissBanner,
|
|
17
|
-
type ErrorBannerAction,
|
|
18
|
-
type ErrorBannerEntry,
|
|
19
21
|
type PushErrorInput,
|
|
20
22
|
} from "./error-banners.js";
|
|
21
23
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import type { ErrorBannerEntry } from "@remit/ui";
|
|
3
4
|
import {
|
|
4
5
|
appendBanner,
|
|
5
|
-
buildEntry,
|
|
6
6
|
buildMutationErrorBanner,
|
|
7
7
|
dismissBanner,
|
|
8
|
-
type ErrorBannerEntry,
|
|
9
8
|
formatErrorDetail,
|
|
10
9
|
isMessageNotFoundError,
|
|
11
10
|
} from "./error-banners.js";
|
|
@@ -20,26 +19,6 @@ const make = (
|
|
|
20
19
|
...overrides,
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
describe("buildEntry", () => {
|
|
24
|
-
test("defaults to error severity", () => {
|
|
25
|
-
const entry = buildEntry({ title: "boom" }, "id-1", 1000);
|
|
26
|
-
assert.equal(entry.severity, "error");
|
|
27
|
-
assert.equal(entry.title, "boom");
|
|
28
|
-
assert.equal(entry.id, "id-1");
|
|
29
|
-
assert.equal(entry.createdAt, 1000);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("preserves explicit severity and detail", () => {
|
|
33
|
-
const entry = buildEntry(
|
|
34
|
-
{ severity: "warning", title: "watch", detail: "context" },
|
|
35
|
-
"id-2",
|
|
36
|
-
2000,
|
|
37
|
-
);
|
|
38
|
-
assert.equal(entry.severity, "warning");
|
|
39
|
-
assert.equal(entry.detail, "context");
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
22
|
describe("appendBanner", () => {
|
|
44
23
|
test("adds an entry at the end of the stack", () => {
|
|
45
24
|
const a = make({ id: "1", title: "first" });
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export interface ErrorBannerAction {
|
|
8
|
-
label: string;
|
|
9
|
-
href: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ErrorBannerEntry {
|
|
13
|
-
id: string;
|
|
14
|
-
severity: ErrorBannerSeverity;
|
|
15
|
-
title: string;
|
|
16
|
-
detail?: string;
|
|
17
|
-
action?: ErrorBannerAction;
|
|
18
|
-
createdAt: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ErrorBannerAction,
|
|
3
|
+
ErrorBannerEntry,
|
|
4
|
+
ErrorBannerSeverity,
|
|
5
|
+
} from "@remit/ui";
|
|
6
|
+
import { codedApiErrorBody } from "@/lib/api";
|
|
21
7
|
export interface PushErrorInput {
|
|
22
8
|
severity?: ErrorBannerSeverity;
|
|
23
9
|
title: string;
|
|
@@ -67,6 +53,9 @@ export const formatErrorDetail = (error: unknown): string | undefined => {
|
|
|
67
53
|
return undefined;
|
|
68
54
|
};
|
|
69
55
|
|
|
56
|
+
export const isMessageGoneFromServerError = (error: unknown): boolean =>
|
|
57
|
+
codedApiErrorBody(error)?.code === "message_gone_from_server";
|
|
58
|
+
|
|
70
59
|
/**
|
|
71
60
|
* Detect a "describeMessage hit a deleted/missing row" error.
|
|
72
61
|
*
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* editor's picker and the address filter all read. A delete or a rezone also
|
|
11
11
|
* moves or removes occurrences, so the event reads are invalidated with it.
|
|
12
12
|
*/
|
|
13
|
+
|
|
13
14
|
import {
|
|
14
15
|
calendarDetailOperationsDeleteCalendarMutation,
|
|
15
16
|
calendarDetailOperationsUpdateCalendarMutation,
|
|
@@ -20,9 +21,9 @@ import type {
|
|
|
20
21
|
RemitImapCreateCalendarInput,
|
|
21
22
|
RemitImapUpdateCalendarInput,
|
|
22
23
|
} from "@remit/api-http-client/types.gen.ts";
|
|
24
|
+
import { formatErrorMessage } from "@remit/ui";
|
|
23
25
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
24
26
|
import { useCallback, useMemo } from "react";
|
|
25
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
26
27
|
import { softErrorStatuses } from "@/lib/error-classifier";
|
|
27
28
|
import { useInvalidateCalendarReads } from "./useCalendarWrites";
|
|
28
29
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* must state from a fault it must not swallow. A lapsed session is neither, and
|
|
12
12
|
* escalates the way it does everywhere else.
|
|
13
13
|
*/
|
|
14
|
+
|
|
14
15
|
import {
|
|
15
16
|
calendarSuggestionActionOperationsAcceptCalendarSuggestionMutation,
|
|
16
17
|
calendarSuggestionActionOperationsDeclineCalendarSuggestionMutation,
|
|
@@ -24,6 +25,7 @@ import type {
|
|
|
24
25
|
RemitImapAcceptCalendarSuggestionInput,
|
|
25
26
|
RemitImapCalendarSuggestionResponse,
|
|
26
27
|
} from "@remit/api-http-client/types.gen.ts";
|
|
28
|
+
import { formatErrorMessage } from "@remit/ui";
|
|
27
29
|
import {
|
|
28
30
|
type QueryClient,
|
|
29
31
|
useMutation,
|
|
@@ -31,7 +33,6 @@ import {
|
|
|
31
33
|
useQueryClient,
|
|
32
34
|
} from "@tanstack/react-query";
|
|
33
35
|
import { useCallback, useMemo } from "react";
|
|
34
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
35
36
|
import { softErrorStatuses } from "@/lib/error-classifier";
|
|
36
37
|
|
|
37
38
|
const SUGGESTION_META = softErrorStatuses(400, 404);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* statuses this surface owns, so a lapsed session takes the screen the way it
|
|
14
14
|
* does everywhere else.
|
|
15
15
|
*/
|
|
16
|
+
|
|
16
17
|
import {
|
|
17
18
|
calendarEventDetailOperationsDeleteCalendarEventMutation,
|
|
18
19
|
calendarEventDetailOperationsUpdateCalendarEventMutation,
|
|
@@ -24,9 +25,9 @@ import type {
|
|
|
24
25
|
RemitImapUpdateCalendarEventInput,
|
|
25
26
|
} from "@remit/api-http-client/types.gen.ts";
|
|
26
27
|
import type { RecurrenceScope } from "@remit/ui";
|
|
28
|
+
import { formatErrorMessage } from "@remit/ui";
|
|
27
29
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
28
30
|
import { useCallback, useMemo } from "react";
|
|
29
|
-
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
30
31
|
import { getErrorStatus, softErrorStatuses } from "@/lib/error-classifier";
|
|
31
32
|
|
|
32
33
|
/** The refusals a calendar surface renders itself. A 401 is not among them. */
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
-
import {
|
|
5
|
-
dropDeletedThreads,
|
|
6
|
-
removeMessagesFromItems,
|
|
7
|
-
} from "./useDeleteMessages.js";
|
|
4
|
+
import { dropDeletedThreads } from "./useDeleteMessages.js";
|
|
8
5
|
|
|
9
6
|
const make = (
|
|
10
7
|
overrides: Partial<RemitImapThreadMessageResponse> & {
|
|
@@ -26,44 +23,6 @@ const make = (
|
|
|
26
23
|
...overrides,
|
|
27
24
|
}) as RemitImapThreadMessageResponse;
|
|
28
25
|
|
|
29
|
-
describe("removeMessagesFromItems", () => {
|
|
30
|
-
test("drops items whose messageId is in the set", () => {
|
|
31
|
-
const items = [
|
|
32
|
-
make({ messageId: "m1", threadMessageId: "tm1" }),
|
|
33
|
-
make({ messageId: "m2", threadMessageId: "tm2" }),
|
|
34
|
-
make({ messageId: "m3", threadMessageId: "tm3" }),
|
|
35
|
-
];
|
|
36
|
-
const got = removeMessagesFromItems(items, new Set(["m2"]));
|
|
37
|
-
assert.deepStrictEqual(
|
|
38
|
-
got.map((i) => i.messageId),
|
|
39
|
-
["m1", "m3"],
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("returns the input unchanged when nothing matches", () => {
|
|
44
|
-
const items = [
|
|
45
|
-
make({ messageId: "m1", threadMessageId: "tm1" }),
|
|
46
|
-
make({ messageId: "m2", threadMessageId: "tm2" }),
|
|
47
|
-
];
|
|
48
|
-
const got = removeMessagesFromItems(items, new Set(["does-not-exist"]));
|
|
49
|
-
assert.equal(got.length, 2);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("returns an empty array when all items match", () => {
|
|
53
|
-
const items = [
|
|
54
|
-
make({ messageId: "m1", threadMessageId: "tm1" }),
|
|
55
|
-
make({ messageId: "m2", threadMessageId: "tm2" }),
|
|
56
|
-
];
|
|
57
|
-
const got = removeMessagesFromItems(items, new Set(["m1", "m2"]));
|
|
58
|
-
assert.deepStrictEqual(got, []);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("returns an empty array for empty input", () => {
|
|
62
|
-
const got = removeMessagesFromItems([], new Set(["m1"]));
|
|
63
|
-
assert.deepStrictEqual(got, []);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
26
|
describe("dropDeletedThreads (#212)", () => {
|
|
68
27
|
// Belt-and-braces UI filter for issue #212. If the backend regresses and
|
|
69
28
|
// returns a soft-deleted row, the inbox list must still hide it. The
|
|
@@ -32,7 +32,6 @@ const filter = (
|
|
|
32
32
|
name: "Travel",
|
|
33
33
|
scope: "Standing",
|
|
34
34
|
state: "Active",
|
|
35
|
-
hasAnchor: false,
|
|
36
35
|
ruleChangedAt: 0,
|
|
37
36
|
actionChangedAt: 0,
|
|
38
37
|
matchOperator: "And",
|
|
@@ -79,13 +78,6 @@ describe("buildFilterListKey", () => {
|
|
|
79
78
|
filterOperationsListFiltersQueryKey({ path: { accountId: ACCOUNT_ID } }),
|
|
80
79
|
);
|
|
81
80
|
});
|
|
82
|
-
|
|
83
|
-
test("is distinct per account, so one account's list is not invalidated by another's write", () => {
|
|
84
|
-
assert.notDeepEqual(
|
|
85
|
-
buildFilterListKey("acc-1"),
|
|
86
|
-
buildFilterListKey("acc-2"),
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
81
|
});
|
|
90
82
|
|
|
91
83
|
describe("useFilterList", () => {
|
|
@@ -12,10 +12,7 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { afterEach, describe, it } from "node:test";
|
|
13
13
|
import { createElement } from "react";
|
|
14
14
|
import { createDomHarness, type DomHarness } from "../test-support/dom";
|
|
15
|
-
import {
|
|
16
|
-
FOLLOW_FOCUS_DELAY_MS,
|
|
17
|
-
useFollowFocusOpen,
|
|
18
|
-
} from "./useFollowFocusOpen";
|
|
15
|
+
import { useFollowFocusOpen } from "./useFollowFocusOpen";
|
|
19
16
|
|
|
20
17
|
let harness: DomHarness | undefined;
|
|
21
18
|
|
|
@@ -146,10 +143,3 @@ describe("useFollowFocusOpen", () => {
|
|
|
146
143
|
assert.deepEqual(driver.opened, []);
|
|
147
144
|
});
|
|
148
145
|
});
|
|
149
|
-
|
|
150
|
-
describe("FOLLOW_FOCUS_DELAY_MS", () => {
|
|
151
|
-
it("sits above key repeat and below what reads as a wait", () => {
|
|
152
|
-
assert.ok(FOLLOW_FOCUS_DELAY_MS >= 60);
|
|
153
|
-
assert.ok(FOLLOW_FOCUS_DELAY_MS <= 250);
|
|
154
|
-
});
|
|
155
|
-
});
|