@remit/web-client 0.0.1
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/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- package/vite.base.ts +43 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { BugReportContext } from "./bug-report";
|
|
4
|
+
import { buildBugReportDetails, buildGitHubIssueUrl } from "./bug-report";
|
|
5
|
+
|
|
6
|
+
const baseCtx: BugReportContext = {
|
|
7
|
+
appSha: "abcdef1234567890abcdef1234567890abcdef12",
|
|
8
|
+
appShortSha: "abcdef1",
|
|
9
|
+
appBuildTime: "2024-01-15T10:30:00.000Z",
|
|
10
|
+
userAgent: "Mozilla/5.0 (Test Browser)",
|
|
11
|
+
viewport: "1440×900",
|
|
12
|
+
timestamp: "2024-06-12T08:00:00.000Z",
|
|
13
|
+
timezone: "Europe/Amsterdam",
|
|
14
|
+
href: "https://app.example.com/mail/inbox?q=test",
|
|
15
|
+
recentErrors: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* URLSearchParams encodes spaces as `+`, but decodeURIComponent does not
|
|
20
|
+
* decode `+` back to space. Replace `+` first so assertions on
|
|
21
|
+
* human-readable strings work as expected.
|
|
22
|
+
*/
|
|
23
|
+
function decode(url: string): string {
|
|
24
|
+
return decodeURIComponent(url.replaceAll("+", " "));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("buildGitHubIssueUrl", () => {
|
|
28
|
+
it("returns a URL pointing at the GitHub new-issue endpoint", () => {
|
|
29
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
30
|
+
assert.ok(
|
|
31
|
+
url.startsWith("https://github.com/remit-mail/remit/issues/new?"),
|
|
32
|
+
`Expected GitHub issues URL, got: ${url}`,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("prefills a title param", () => {
|
|
37
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
38
|
+
assert.ok(url.includes("title="), "Expected a title param");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("includes the short SHA in the body", () => {
|
|
42
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
43
|
+
const decoded = decode(url);
|
|
44
|
+
assert.ok(decoded.includes("abcdef1"), "Expected short SHA in body");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("includes the full SHA commit link in the body", () => {
|
|
48
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
49
|
+
const decoded = decode(url);
|
|
50
|
+
assert.ok(
|
|
51
|
+
decoded.includes("abcdef1234567890abcdef1234567890abcdef12"),
|
|
52
|
+
"Expected full SHA in commit link",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("includes build time in the body", () => {
|
|
57
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
58
|
+
const decoded = decode(url);
|
|
59
|
+
assert.ok(
|
|
60
|
+
decoded.includes("2024-01-15T10:30:00.000Z"),
|
|
61
|
+
"Expected build time in body",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("includes the user agent", () => {
|
|
66
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
67
|
+
const decoded = decode(url);
|
|
68
|
+
assert.ok(
|
|
69
|
+
decoded.includes("Mozilla/5.0 (Test Browser)"),
|
|
70
|
+
"Expected userAgent in body",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("includes the viewport", () => {
|
|
75
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
76
|
+
const decoded = decode(url);
|
|
77
|
+
assert.ok(decoded.includes("1440×900"), "Expected viewport in body");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("includes the timestamp", () => {
|
|
81
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
82
|
+
const decoded = decode(url);
|
|
83
|
+
assert.ok(
|
|
84
|
+
decoded.includes("2024-06-12T08:00:00.000Z"),
|
|
85
|
+
"Expected timestamp in body",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("includes the timezone", () => {
|
|
90
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
91
|
+
const decoded = decode(url);
|
|
92
|
+
assert.ok(
|
|
93
|
+
decoded.includes("Europe/Amsterdam"),
|
|
94
|
+
"Expected timezone in body",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("includes the page URL", () => {
|
|
99
|
+
const url = buildGitHubIssueUrl(baseCtx);
|
|
100
|
+
const decoded = decode(url);
|
|
101
|
+
assert.ok(
|
|
102
|
+
decoded.includes("https://app.example.com/mail/inbox"),
|
|
103
|
+
"Expected page href in body",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shows (none) when there are no recent errors", () => {
|
|
108
|
+
const url = buildGitHubIssueUrl({ ...baseCtx, recentErrors: [] });
|
|
109
|
+
const decoded = decode(url);
|
|
110
|
+
assert.ok(decoded.includes("(none)"), "Expected (none) for empty errors");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("includes recent console errors when present", () => {
|
|
114
|
+
const ctx: BugReportContext = {
|
|
115
|
+
...baseCtx,
|
|
116
|
+
recentErrors: [
|
|
117
|
+
"TypeError: Cannot read property x of undefined",
|
|
118
|
+
"Network error: 503",
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
const url = buildGitHubIssueUrl(ctx);
|
|
122
|
+
const decoded = decode(url);
|
|
123
|
+
assert.ok(
|
|
124
|
+
decoded.includes("TypeError: Cannot read property"),
|
|
125
|
+
"Expected first error in body",
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
decoded.includes("Network error: 503"),
|
|
129
|
+
"Expected second error in body",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("keeps the URL parseable after encoding", () => {
|
|
134
|
+
const ctx: BugReportContext = {
|
|
135
|
+
...baseCtx,
|
|
136
|
+
recentErrors: ["Error: something & weird <happened>"],
|
|
137
|
+
};
|
|
138
|
+
const url = buildGitHubIssueUrl(ctx);
|
|
139
|
+
assert.doesNotThrow(() => new URL(url), "URL must be valid");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("buildGitHubIssueUrl — seeded from a caught error", () => {
|
|
144
|
+
const seededCtx: BugReportContext = {
|
|
145
|
+
...baseCtx,
|
|
146
|
+
errorMessage: "date value is not finite in DateTimeFormat format()",
|
|
147
|
+
stack: "Error: date value is not finite\n at format (util.ts:12)",
|
|
148
|
+
componentStack: "\n at AccountsSettings\n at Route",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
it("puts the error message in the title", () => {
|
|
152
|
+
const url = buildGitHubIssueUrl(seededCtx);
|
|
153
|
+
const decoded = decode(url);
|
|
154
|
+
assert.ok(
|
|
155
|
+
decoded.includes("title=Bug: date value is not finite"),
|
|
156
|
+
"Expected the error message in the issue title",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("includes the stacktrace and component stack in the body", () => {
|
|
161
|
+
const decoded = decode(buildGitHubIssueUrl(seededCtx));
|
|
162
|
+
assert.ok(
|
|
163
|
+
decoded.includes("## Stacktrace"),
|
|
164
|
+
"Expected a stacktrace section",
|
|
165
|
+
);
|
|
166
|
+
assert.ok(
|
|
167
|
+
decoded.includes("at format (util.ts:12)"),
|
|
168
|
+
"Expected the stack frame in the body",
|
|
169
|
+
);
|
|
170
|
+
assert.ok(
|
|
171
|
+
decoded.includes("## Component stack"),
|
|
172
|
+
"Expected a component-stack section",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("buildGitHubIssueUrl — truncation to fit the URL budget", () => {
|
|
178
|
+
const hugeStack = `Error: boom\n${" at frame (file.ts:1)\n".repeat(2000)}`;
|
|
179
|
+
const hugeCtx: BugReportContext = {
|
|
180
|
+
...baseCtx,
|
|
181
|
+
errorMessage: "boom",
|
|
182
|
+
stack: hugeStack,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
it("keeps the truncated URL under the GitHub length limit", () => {
|
|
186
|
+
const url = buildGitHubIssueUrl(hugeCtx);
|
|
187
|
+
assert.ok(
|
|
188
|
+
url.length <= 8000,
|
|
189
|
+
`Expected a URL under 8000 chars, got ${url.length}`,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("marks the body as truncated and stays a valid URL", () => {
|
|
194
|
+
const url = buildGitHubIssueUrl(hugeCtx);
|
|
195
|
+
assert.ok(
|
|
196
|
+
decode(url).includes("truncated"),
|
|
197
|
+
"Expected a truncation marker",
|
|
198
|
+
);
|
|
199
|
+
assert.doesNotThrow(() => new URL(url), "Truncated URL must be valid");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("Copy full details keeps the whole stacktrace (nothing lost)", () => {
|
|
203
|
+
const details = buildBugReportDetails(hugeCtx);
|
|
204
|
+
assert.ok(
|
|
205
|
+
details.includes(hugeStack),
|
|
206
|
+
"Expected the full untruncated stack in the copy-details report",
|
|
207
|
+
);
|
|
208
|
+
assert.ok(
|
|
209
|
+
details.length > buildGitHubIssueUrl(hugeCtx).length,
|
|
210
|
+
"Full details should be longer than the truncated URL body",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APP_BUILD_TIME,
|
|
3
|
+
APP_SHA,
|
|
4
|
+
APP_SHORT_SHA,
|
|
5
|
+
GITHUB_NEW_ISSUE_URL,
|
|
6
|
+
} from "./app-info";
|
|
7
|
+
import { getRecentErrors } from "./console-errors";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A GitHub new-issue URL is capped (~8 KB). We budget below that so the stack
|
|
11
|
+
* is truncated to fit rather than producing a URL GitHub silently rejects. The
|
|
12
|
+
* untruncated report is always available via `buildBugReportDetails` (the
|
|
13
|
+
* overlay's "Copy full details" action).
|
|
14
|
+
*/
|
|
15
|
+
const MAX_ISSUE_URL_LENGTH = 7500;
|
|
16
|
+
|
|
17
|
+
const TRUNCATION_MARKER =
|
|
18
|
+
"\n… (truncated — use “Copy full details” for the full stacktrace)";
|
|
19
|
+
|
|
20
|
+
export interface BugReportContext {
|
|
21
|
+
appSha: string;
|
|
22
|
+
appShortSha: string;
|
|
23
|
+
appBuildTime: string;
|
|
24
|
+
userAgent: string;
|
|
25
|
+
viewport: string;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
timezone: string;
|
|
28
|
+
href: string;
|
|
29
|
+
recentErrors: readonly string[];
|
|
30
|
+
/** The message of the error that triggered the report, when seeded from one. */
|
|
31
|
+
errorMessage?: string;
|
|
32
|
+
/** The error's stacktrace, when available. */
|
|
33
|
+
stack?: string;
|
|
34
|
+
/** React component stack from the error boundary, when available. */
|
|
35
|
+
componentStack?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fields callers can seed from a caught error. */
|
|
39
|
+
export interface BugReportSeed {
|
|
40
|
+
errorMessage?: string;
|
|
41
|
+
stack?: string;
|
|
42
|
+
componentStack?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildBugReportContext(seed?: BugReportSeed): BugReportContext {
|
|
46
|
+
return {
|
|
47
|
+
appSha: APP_SHA,
|
|
48
|
+
appShortSha: APP_SHORT_SHA,
|
|
49
|
+
appBuildTime: APP_BUILD_TIME,
|
|
50
|
+
userAgent: navigator.userAgent,
|
|
51
|
+
viewport: `${window.innerWidth}×${window.innerHeight}`,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
54
|
+
href: window.location.href,
|
|
55
|
+
recentErrors: getRecentErrors(),
|
|
56
|
+
errorMessage: seed?.errorMessage,
|
|
57
|
+
stack: seed?.stack,
|
|
58
|
+
componentStack: seed?.componentStack,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fencedBlock(content: string): string {
|
|
63
|
+
return ["```", content, "```"].join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildIssueBody(ctx: BugReportContext, stack?: string): string {
|
|
67
|
+
const errorSection =
|
|
68
|
+
ctx.recentErrors.length > 0
|
|
69
|
+
? ctx.recentErrors.map((e) => ` - ${e}`).join("\n")
|
|
70
|
+
: " (none)";
|
|
71
|
+
|
|
72
|
+
const lines: string[] = [
|
|
73
|
+
"## Environment",
|
|
74
|
+
`- **Version**: [\`${ctx.appShortSha}\`](https://github.com/remit-mail/remit/commit/${ctx.appSha}) built ${ctx.appBuildTime}`,
|
|
75
|
+
`- **Browser**: ${ctx.userAgent}`,
|
|
76
|
+
`- **Viewport**: ${ctx.viewport}`,
|
|
77
|
+
`- **Time**: ${ctx.timestamp} (${ctx.timezone})`,
|
|
78
|
+
"",
|
|
79
|
+
"## URL",
|
|
80
|
+
ctx.href,
|
|
81
|
+
"",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
if (ctx.errorMessage) {
|
|
85
|
+
lines.push("## Error", ctx.errorMessage, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolvedStack = stack ?? ctx.stack;
|
|
89
|
+
if (resolvedStack) {
|
|
90
|
+
lines.push("## Stacktrace", fencedBlock(resolvedStack), "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ctx.componentStack) {
|
|
94
|
+
lines.push("## Component stack", fencedBlock(ctx.componentStack), "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lines.push(
|
|
98
|
+
"## Recent console errors",
|
|
99
|
+
errorSection,
|
|
100
|
+
"",
|
|
101
|
+
"## Steps to reproduce",
|
|
102
|
+
"1. ",
|
|
103
|
+
"",
|
|
104
|
+
"## Expected behaviour",
|
|
105
|
+
"",
|
|
106
|
+
"## Actual behaviour",
|
|
107
|
+
"",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildIssueTitle(ctx: BugReportContext): string {
|
|
114
|
+
if (!ctx.errorMessage) return "Bug: ";
|
|
115
|
+
const firstLine = ctx.errorMessage.split("\n")[0].trim();
|
|
116
|
+
const clipped =
|
|
117
|
+
firstLine.length > 120 ? `${firstLine.slice(0, 117)}…` : firstLine;
|
|
118
|
+
return `Bug: ${clipped}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function issueUrl(ctx: BugReportContext, stack?: string): string {
|
|
122
|
+
const params = new URLSearchParams({
|
|
123
|
+
title: buildIssueTitle(ctx),
|
|
124
|
+
body: buildIssueBody(ctx, stack),
|
|
125
|
+
});
|
|
126
|
+
return `${GITHUB_NEW_ISSUE_URL}?${params.toString()}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The full, untruncated report — every field including the complete stacktrace
|
|
131
|
+
* and component stack. Backs the overlay's "Copy full details" action so
|
|
132
|
+
* nothing is lost when the URL-bound report has to truncate the stack.
|
|
133
|
+
*/
|
|
134
|
+
export function buildBugReportDetails(ctx: BugReportContext): string {
|
|
135
|
+
return buildIssueBody(ctx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build a prefilled GitHub new-issue URL. When the full body would exceed the
|
|
140
|
+
* URL budget, the stacktrace is truncated (binary-searched to the longest
|
|
141
|
+
* prefix that fits) and marked; everything else is preserved. The component
|
|
142
|
+
* stack is dropped from the URL when truncation kicks in — it survives in the
|
|
143
|
+
* "Copy full details" report.
|
|
144
|
+
*/
|
|
145
|
+
export function buildGitHubIssueUrl(ctx: BugReportContext): string {
|
|
146
|
+
const full = issueUrl(ctx);
|
|
147
|
+
if (full.length <= MAX_ISSUE_URL_LENGTH) return full;
|
|
148
|
+
|
|
149
|
+
const stack = ctx.stack ?? "";
|
|
150
|
+
const withoutComponentStack: BugReportContext = {
|
|
151
|
+
...ctx,
|
|
152
|
+
componentStack: undefined,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let lo = 0;
|
|
156
|
+
let hi = stack.length;
|
|
157
|
+
let best = 0;
|
|
158
|
+
while (lo <= hi) {
|
|
159
|
+
const mid = (lo + hi) >> 1;
|
|
160
|
+
const candidate = issueUrl(
|
|
161
|
+
withoutComponentStack,
|
|
162
|
+
stack.slice(0, mid) + TRUNCATION_MARKER,
|
|
163
|
+
);
|
|
164
|
+
if (candidate.length <= MAX_ISSUE_URL_LENGTH) {
|
|
165
|
+
best = mid;
|
|
166
|
+
lo = mid + 1;
|
|
167
|
+
} else {
|
|
168
|
+
hi = mid - 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return issueUrl(
|
|
173
|
+
withoutComponentStack,
|
|
174
|
+
stack.slice(0, best) + TRUNCATION_MARKER,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { client } from "@remit/api-http-client/client.gen.ts";
|
|
2
|
+
import { getRuntimeConfig } from "../runtime-config";
|
|
3
|
+
import { ApiError } from "./api";
|
|
4
|
+
|
|
5
|
+
// In production, the config.js apiUrl points at the deployed API Gateway.
|
|
6
|
+
// In local dev, the Vite proxy forwards /api -> localhost:4321 (see vite.config.ts).
|
|
7
|
+
const baseUrl = getRuntimeConfig().apiUrl;
|
|
8
|
+
|
|
9
|
+
client.setConfig({
|
|
10
|
+
baseUrl,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The hey-api client throws the parsed JSON error *body* on a non-ok response —
|
|
15
|
+
* a shape with no HTTP status. That makes it impossible to tell a 404 (expected)
|
|
16
|
+
* from a 500 (fatal) at the call site, which is the whole point of the fail-fast
|
|
17
|
+
* classifier. This error interceptor re-wraps every HTTP error as an `ApiError`
|
|
18
|
+
* carrying `response.status`, so `shouldEscalate` / `getErrorStatus` work
|
|
19
|
+
* uniformly across both clients. The body's `message` is preserved on the
|
|
20
|
+
* `ApiError` so existing message-prefix checks (e.g. `isMessageNotFoundError`)
|
|
21
|
+
* keep working.
|
|
22
|
+
*/
|
|
23
|
+
client.interceptors.error.use((error, response) => {
|
|
24
|
+
if (error instanceof ApiError) return error;
|
|
25
|
+
if (!response) return error;
|
|
26
|
+
|
|
27
|
+
const body = error;
|
|
28
|
+
const message =
|
|
29
|
+
body && typeof body === "object" && "message" in body
|
|
30
|
+
? String((body as { message: unknown }).message)
|
|
31
|
+
: `Request failed with status ${response.status}`;
|
|
32
|
+
|
|
33
|
+
return new ApiError(message, response.status, body);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export { client };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import { install } from "./console-errors";
|
|
4
|
+
import { __resetFatalError, subscribeFatalError } from "./fatal-error";
|
|
5
|
+
|
|
6
|
+
type WindowListener = (event: unknown) => void;
|
|
7
|
+
|
|
8
|
+
interface WindowStub {
|
|
9
|
+
listeners: Map<string, WindowListener>;
|
|
10
|
+
addEventListener(type: string, listener: WindowListener): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function installWindowStub(): WindowStub {
|
|
14
|
+
const stub: WindowStub = {
|
|
15
|
+
listeners: new Map(),
|
|
16
|
+
addEventListener(type, listener): void {
|
|
17
|
+
this.listeners.set(type, listener);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
(globalThis as unknown as { window: WindowStub }).window = stub;
|
|
21
|
+
return stub;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emitWindowError(stub: WindowStub, message: string): void {
|
|
25
|
+
const handler = stub.listeners.get("error");
|
|
26
|
+
assert.ok(handler, "install() should register a window error handler");
|
|
27
|
+
handler({ message, error: null, filename: "app.js", lineno: 0 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("console-errors window error handling", () => {
|
|
31
|
+
const originalConsoleError = console.error;
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
__resetFatalError();
|
|
35
|
+
console.error = originalConsoleError;
|
|
36
|
+
delete (globalThis as unknown as { window?: unknown }).window;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not escalate benign ResizeObserver noise", () => {
|
|
40
|
+
const stub = installWindowStub();
|
|
41
|
+
const escalated: string[] = [];
|
|
42
|
+
subscribeFatalError((fatal) => escalated.push(fatal.message));
|
|
43
|
+
install();
|
|
44
|
+
|
|
45
|
+
emitWindowError(stub, "ResizeObserver loop limit exceeded");
|
|
46
|
+
emitWindowError(
|
|
47
|
+
stub,
|
|
48
|
+
"ResizeObserver loop completed with undelivered notifications.",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(escalated, []);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("escalates an arbitrary uncaught error", () => {
|
|
55
|
+
const stub = installWindowStub();
|
|
56
|
+
const escalated: string[] = [];
|
|
57
|
+
subscribeFatalError((fatal) => escalated.push(fatal.message));
|
|
58
|
+
install();
|
|
59
|
+
|
|
60
|
+
emitWindowError(stub, "TypeError: boom");
|
|
61
|
+
|
|
62
|
+
assert.equal(escalated.length, 1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny ring-buffer that captures the last N console errors, uncaught
|
|
3
|
+
* exceptions, and unhandled promise rejections. Install once at app
|
|
4
|
+
* startup; consumers call getRecentErrors() to read the buffer.
|
|
5
|
+
*
|
|
6
|
+
* Dependency-free and side-effect-free until install() is called.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { reportFatalError } from "./fatal-error";
|
|
10
|
+
|
|
11
|
+
const MAX_ENTRIES = 20;
|
|
12
|
+
|
|
13
|
+
const ring: string[] = [];
|
|
14
|
+
|
|
15
|
+
// Browser-spec noise, not app errors: the ResizeObserver spec fires these when
|
|
16
|
+
// observation callbacks don't settle within a frame. They surface as window
|
|
17
|
+
// `error` events with a null `error` and are safe to ignore. The live message
|
|
18
|
+
// carries a trailing period, so strip it before matching this canonical set.
|
|
19
|
+
const BENIGN_RESIZE_OBSERVER_MESSAGES = new Set([
|
|
20
|
+
"ResizeObserver loop limit exceeded",
|
|
21
|
+
"ResizeObserver loop completed with undelivered notifications",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function isBenignResizeObserverError(message: unknown): boolean {
|
|
25
|
+
if (typeof message !== "string") return false;
|
|
26
|
+
return BENIGN_RESIZE_OBSERVER_MESSAGES.has(message.replace(/\.\s*$/, ""));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function push(entry: string): void {
|
|
30
|
+
ring.push(entry);
|
|
31
|
+
if (ring.length > MAX_ENTRIES) ring.shift();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a single line into the recent-errors ring. Exposed so the global
|
|
36
|
+
* `reportFatalError` seam (lib/fatal-error.ts) can land fatal errors in the
|
|
37
|
+
* same buffer the bug-report flow reads, without re-implementing the ring.
|
|
38
|
+
*/
|
|
39
|
+
export function recordError(entry: string): void {
|
|
40
|
+
push(entry);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatArgs(args: unknown[]): string {
|
|
44
|
+
return args
|
|
45
|
+
.map((a) => {
|
|
46
|
+
if (typeof a === "string") return a;
|
|
47
|
+
if (a instanceof Error) return `${a.name}: ${a.message}`;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(a);
|
|
50
|
+
} catch {
|
|
51
|
+
return String(a);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function install(): void {
|
|
58
|
+
const originalError = console.error.bind(console);
|
|
59
|
+
console.error = (...args: unknown[]) => {
|
|
60
|
+
push(formatArgs(args));
|
|
61
|
+
originalError(...args);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
window.addEventListener("error", (event: ErrorEvent) => {
|
|
65
|
+
if (isBenignResizeObserverError(event.message)) return;
|
|
66
|
+
push(
|
|
67
|
+
`Uncaught ${event.error instanceof Error ? `${event.error.name}: ${event.error.message}` : String(event.message)} (${event.filename}:${event.lineno})`,
|
|
68
|
+
);
|
|
69
|
+
// An uncaught exception is, by definition, unhandled — escalate it.
|
|
70
|
+
reportFatalError(event.error ?? event.message);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
window.addEventListener(
|
|
74
|
+
"unhandledrejection",
|
|
75
|
+
(event: PromiseRejectionEvent) => {
|
|
76
|
+
const reason = event.reason;
|
|
77
|
+
const detail =
|
|
78
|
+
reason instanceof Error
|
|
79
|
+
? `${reason.name}: ${reason.message}`
|
|
80
|
+
: String(reason);
|
|
81
|
+
push(`Unhandled rejection: ${detail}`);
|
|
82
|
+
// A rejection that bubbles to the window was never handled by any
|
|
83
|
+
// caller — route it through the same fatal seam.
|
|
84
|
+
reportFatalError(reason);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns a copy of the captured entries (oldest first, up to 20). */
|
|
90
|
+
export function getRecentErrors(): readonly string[] {
|
|
91
|
+
return ring.slice();
|
|
92
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
+
import { buildConversationTarget } from "./conversation-target.js";
|
|
5
|
+
|
|
6
|
+
function thread(
|
|
7
|
+
overrides: Partial<RemitImapThreadMessageResponse> = {},
|
|
8
|
+
): RemitImapThreadMessageResponse {
|
|
9
|
+
return {
|
|
10
|
+
messageId: "m1",
|
|
11
|
+
threadId: "t1",
|
|
12
|
+
mailboxId: "mb1",
|
|
13
|
+
subject: "Loaded subject",
|
|
14
|
+
isRead: false,
|
|
15
|
+
...overrides,
|
|
16
|
+
} as RemitImapThreadMessageResponse;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("buildConversationTarget", () => {
|
|
20
|
+
test("prefers the loaded thread, carrying its display fields", () => {
|
|
21
|
+
const authenticity = { dkimMismatch: true, fromDomain: "example.com" };
|
|
22
|
+
const target = buildConversationTarget(thread({ authenticity }), {
|
|
23
|
+
messageId: "m1",
|
|
24
|
+
threadId: "ignored",
|
|
25
|
+
mailboxId: "ignored",
|
|
26
|
+
});
|
|
27
|
+
assert.deepEqual(target, {
|
|
28
|
+
threadId: "t1",
|
|
29
|
+
mailboxId: "mb1",
|
|
30
|
+
subject: "Loaded subject",
|
|
31
|
+
messageId: "m1",
|
|
32
|
+
authenticity,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// The bug: a tapped semantic "Related" hit's message isn't in the loaded list,
|
|
37
|
+
// so there is no thread to resolve — it must still open from the URL hints.
|
|
38
|
+
test("falls back to the URL thread + mailbox when no thread is loaded", () => {
|
|
39
|
+
const target = buildConversationTarget(undefined, {
|
|
40
|
+
messageId: "m9",
|
|
41
|
+
threadId: "t9",
|
|
42
|
+
mailboxId: "mb9",
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(target, {
|
|
45
|
+
threadId: "t9",
|
|
46
|
+
mailboxId: "mb9",
|
|
47
|
+
messageId: "m9",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("returns undefined when the message is cleared (Back pressed)", () => {
|
|
52
|
+
const target = buildConversationTarget(undefined, {
|
|
53
|
+
messageId: undefined,
|
|
54
|
+
threadId: "t9",
|
|
55
|
+
mailboxId: "mb9",
|
|
56
|
+
});
|
|
57
|
+
assert.equal(target, undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns undefined without a threadId to open", () => {
|
|
61
|
+
const target = buildConversationTarget(undefined, {
|
|
62
|
+
messageId: "m9",
|
|
63
|
+
threadId: undefined,
|
|
64
|
+
mailboxId: "mb9",
|
|
65
|
+
});
|
|
66
|
+
assert.equal(target, undefined);
|
|
67
|
+
});
|
|
68
|
+
});
|