@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
|
@@ -97,21 +97,6 @@ describe("buildSenderIntel", () => {
|
|
|
97
97
|
const result = buildSenderIntel(thread, address);
|
|
98
98
|
assert.equal(result.inboundCount, 34);
|
|
99
99
|
});
|
|
100
|
-
|
|
101
|
-
test("surfaces replyCount from address", () => {
|
|
102
|
-
const thread = makeThread();
|
|
103
|
-
const address = makeAddress({ inboundCount: 34, replyCount: 12 });
|
|
104
|
-
const result = buildSenderIntel(thread, address);
|
|
105
|
-
assert.equal(result.replyCount, 12);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("surfaces both counters together", () => {
|
|
109
|
-
const thread = makeThread();
|
|
110
|
-
const address = makeAddress({ inboundCount: 34, replyCount: 12 });
|
|
111
|
-
const result = buildSenderIntel(thread, address);
|
|
112
|
-
assert.equal(result.inboundCount, 34);
|
|
113
|
-
assert.equal(result.replyCount, 12);
|
|
114
|
-
});
|
|
115
100
|
});
|
|
116
101
|
|
|
117
102
|
describe("suppression — counters absent", () => {
|
|
@@ -123,13 +108,6 @@ describe("buildSenderIntel", () => {
|
|
|
123
108
|
assert.equal(result.inboundCount, undefined);
|
|
124
109
|
});
|
|
125
110
|
|
|
126
|
-
test("replyCount is undefined when address has no replyCount", () => {
|
|
127
|
-
const thread = makeThread();
|
|
128
|
-
const address = makeAddress({ inboundCount: 5 }); // no replyCount
|
|
129
|
-
const result = buildSenderIntel(thread, address);
|
|
130
|
-
assert.equal(result.replyCount, undefined);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
111
|
test("both counters are undefined when address is undefined", () => {
|
|
134
112
|
const thread = makeThread();
|
|
135
113
|
const result = buildSenderIntel(thread, undefined);
|
|
@@ -173,12 +151,6 @@ describe("buildSenderIntel", () => {
|
|
|
173
151
|
assert.equal(result.name, "Unknown");
|
|
174
152
|
});
|
|
175
153
|
|
|
176
|
-
test("passes senderTrust through", () => {
|
|
177
|
-
const thread = makeThread({ senderTrust: "vip" });
|
|
178
|
-
const result = buildSenderIntel(thread, undefined);
|
|
179
|
-
assert.equal(result.trust, "vip");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
154
|
test("firstSeenLabel is 'unknown' when no address", () => {
|
|
183
155
|
const thread = makeThread();
|
|
184
156
|
const result = buildSenderIntel(thread, undefined);
|
|
@@ -225,7 +197,7 @@ describe("buildSenderIntel", () => {
|
|
|
225
197
|
describe("buildAuthenticityIntel", () => {
|
|
226
198
|
test("caution when there is no authenticity signal", () => {
|
|
227
199
|
const thread = makeThread({ fromEmail: "alice@example.com" });
|
|
228
|
-
const result = buildAuthenticityIntel(thread
|
|
200
|
+
const result = buildAuthenticityIntel(thread);
|
|
229
201
|
assert.equal(result.verdict, "caution");
|
|
230
202
|
assert.match(result.summary, /can't verify the sender/);
|
|
231
203
|
});
|
|
@@ -239,7 +211,7 @@ describe("buildAuthenticityIntel", () => {
|
|
|
239
211
|
dkimMismatch: false,
|
|
240
212
|
},
|
|
241
213
|
} as Partial<RemitImapThreadMessageResponse>);
|
|
242
|
-
const result = buildAuthenticityIntel(thread
|
|
214
|
+
const result = buildAuthenticityIntel(thread);
|
|
243
215
|
assert.equal(result.verdict, "aligned");
|
|
244
216
|
});
|
|
245
217
|
|
|
@@ -253,19 +225,15 @@ describe("buildAuthenticityIntel", () => {
|
|
|
253
225
|
dkimMismatch: true,
|
|
254
226
|
},
|
|
255
227
|
} as Partial<RemitImapThreadMessageResponse>);
|
|
256
|
-
const result = buildAuthenticityIntel(thread
|
|
228
|
+
const result = buildAuthenticityIntel(thread);
|
|
257
229
|
assert.equal(result.verdict, "mismatch");
|
|
258
230
|
assert.equal(result.addressUnreadable, undefined);
|
|
259
|
-
assert.equal(result.similarCount, 3);
|
|
260
231
|
});
|
|
261
232
|
|
|
262
233
|
test("no user-facing copy names the verification mechanism", () => {
|
|
263
234
|
const verdicts = [
|
|
264
|
-
buildAuthenticityIntel(makeThread()
|
|
265
|
-
buildAuthenticityIntel(
|
|
266
|
-
makeThread({ fromEmail: "x@no-signal.example" }),
|
|
267
|
-
0,
|
|
268
|
-
),
|
|
235
|
+
buildAuthenticityIntel(makeThread()),
|
|
236
|
+
buildAuthenticityIntel(makeThread({ fromEmail: "x@no-signal.example" })),
|
|
269
237
|
buildAuthenticityIntel(
|
|
270
238
|
makeThread({
|
|
271
239
|
fromEmail: "a@bank.example",
|
|
@@ -275,7 +243,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
275
243
|
dkimMismatch: true,
|
|
276
244
|
},
|
|
277
245
|
} as Partial<RemitImapThreadMessageResponse>),
|
|
278
|
-
0,
|
|
279
246
|
),
|
|
280
247
|
];
|
|
281
248
|
for (const v of verdicts) {
|
|
@@ -305,7 +272,7 @@ describe("buildAuthenticityIntel", () => {
|
|
|
305
272
|
} as Partial<RemitImapThreadMessageResponse>);
|
|
306
273
|
|
|
307
274
|
test("is never presented as verified", () => {
|
|
308
|
-
const result = buildAuthenticityIntel(infoMedics
|
|
275
|
+
const result = buildAuthenticityIntel(infoMedics);
|
|
309
276
|
assert.notEqual(result.verdict, "aligned");
|
|
310
277
|
assert.doesNotMatch(result.summary, /We verified/i);
|
|
311
278
|
});
|
|
@@ -315,7 +282,7 @@ describe("buildAuthenticityIntel", () => {
|
|
|
315
282
|
// signed by a relay or ESP infrastructure domain must not read as
|
|
316
283
|
// "the name looks nothing like <that other party>".
|
|
317
284
|
test("leads with the concern, naming the domain the name was actually compared to, and the link destination", () => {
|
|
318
|
-
const result = buildAuthenticityIntel(infoMedics
|
|
285
|
+
const result = buildAuthenticityIntel(infoMedics);
|
|
319
286
|
assert.equal(result.verdict, "caution");
|
|
320
287
|
assert.match(result.summary, /^The name it shows/);
|
|
321
288
|
assert.match(result.summary, /"InfoMedics"/);
|
|
@@ -338,7 +305,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
338
305
|
offDomainLinkDomains: [],
|
|
339
306
|
},
|
|
340
307
|
} as Partial<RemitImapThreadMessageResponse>),
|
|
341
|
-
0,
|
|
342
308
|
);
|
|
343
309
|
assert.equal(result.verdict, "caution");
|
|
344
310
|
assert.match(result.summary, /only looks like/);
|
|
@@ -357,7 +323,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
357
323
|
offDomainLinkDomains: [],
|
|
358
324
|
},
|
|
359
325
|
} as Partial<RemitImapThreadMessageResponse>),
|
|
360
|
-
0,
|
|
361
326
|
);
|
|
362
327
|
assert.equal(result.verdict, "caution");
|
|
363
328
|
assert.match(result.summary, /"matthijs@ischen\.nl"/);
|
|
@@ -377,7 +342,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
377
342
|
offDomainLinkDomains: [],
|
|
378
343
|
},
|
|
379
344
|
} as Partial<RemitImapThreadMessageResponse>),
|
|
380
|
-
0,
|
|
381
345
|
);
|
|
382
346
|
assert.equal(result.verdict, "aligned");
|
|
383
347
|
});
|
|
@@ -392,7 +356,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
392
356
|
dkimMismatch: false,
|
|
393
357
|
},
|
|
394
358
|
} as Partial<RemitImapThreadMessageResponse>),
|
|
395
|
-
0,
|
|
396
359
|
);
|
|
397
360
|
assert.equal(result.verdict, "aligned");
|
|
398
361
|
});
|
|
@@ -409,7 +372,6 @@ describe("buildAuthenticityIntel", () => {
|
|
|
409
372
|
offDomainLinkDomains: "elsewhere.example",
|
|
410
373
|
},
|
|
411
374
|
} as unknown as Partial<RemitImapThreadMessageResponse>),
|
|
412
|
-
0,
|
|
413
375
|
);
|
|
414
376
|
assert.equal(result.verdict, "aligned");
|
|
415
377
|
});
|
|
@@ -420,7 +382,7 @@ describe("buildAuthenticityIntel", () => {
|
|
|
420
382
|
const thread = makeThread({
|
|
421
383
|
fromEmail: "missing_mailbox@missing_domain",
|
|
422
384
|
});
|
|
423
|
-
const result = buildAuthenticityIntel(thread
|
|
385
|
+
const result = buildAuthenticityIntel(thread);
|
|
424
386
|
assert.equal(result.verdict, "mismatch");
|
|
425
387
|
assert.equal(result.addressUnreadable, true);
|
|
426
388
|
});
|
|
@@ -433,7 +395,7 @@ describe("buildAuthenticityIntel", () => {
|
|
|
433
395
|
dkimMismatch: false,
|
|
434
396
|
},
|
|
435
397
|
} as Partial<RemitImapThreadMessageResponse>);
|
|
436
|
-
const result = buildAuthenticityIntel(thread
|
|
398
|
+
const result = buildAuthenticityIntel(thread);
|
|
437
399
|
assert.equal(result.verdict, "mismatch");
|
|
438
400
|
assert.equal(result.addressUnreadable, true);
|
|
439
401
|
});
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addressOperationsSearchAddressesOptions,
|
|
3
|
-
semanticSearchOperationsSemanticSearchOptions,
|
|
4
|
-
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
1
|
+
import { addressOperationsSearchAddressesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
5
2
|
import type {
|
|
6
3
|
RemitImapAddressResponse,
|
|
7
4
|
RemitImapThreadMessageResponse,
|
|
@@ -10,17 +7,14 @@ import { DisplayNameCorrespondence } from "@remit/domain-enums";
|
|
|
10
7
|
import type {
|
|
11
8
|
AuthenticityIntel,
|
|
12
9
|
IntelligenceData,
|
|
13
|
-
MatchedChunk,
|
|
14
10
|
SenderFlagsIntel,
|
|
15
11
|
SenderIntel,
|
|
16
|
-
SimilarMessageIntel,
|
|
17
12
|
} from "@remit/ui";
|
|
18
13
|
import { isThreadCategory } from "@remit/ui";
|
|
19
14
|
import { useQuery } from "@tanstack/react-query";
|
|
20
15
|
import { useMemo } from "react";
|
|
21
|
-
import { useSemanticSearchEnabled } from "@/hooks/useSemanticSearchEnabled";
|
|
22
16
|
import { isServerError } from "@/lib/error-classifier";
|
|
23
|
-
import { formatDate
|
|
17
|
+
import { formatDate } from "@/lib/format";
|
|
24
18
|
import {
|
|
25
19
|
pickSenderAddress,
|
|
26
20
|
senderAddressSearchQuery,
|
|
@@ -194,27 +188,16 @@ function readDomainList(value: unknown): string[] {
|
|
|
194
188
|
);
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
/**
|
|
198
|
-
* Build authenticity intel from the thread message's authenticity field.
|
|
199
|
-
*
|
|
200
|
-
* `similarCount` is the number of semantic-search lookalikes; the panel uses
|
|
201
|
-
* it to render the "N similar messages" campaign-reveal CTA inside the
|
|
202
|
-
* danger panel. Passed in because it is derived from the semantic-search
|
|
203
|
-
* result, not the thread row.
|
|
204
|
-
*/
|
|
191
|
+
/** Build authenticity intel from the thread message's authenticity field. */
|
|
205
192
|
export function buildAuthenticityIntel(
|
|
206
193
|
thread: RemitImapThreadMessageResponse,
|
|
207
|
-
similarCount: number,
|
|
208
194
|
): AuthenticityIntel {
|
|
209
|
-
const surfacedSimilar = similarCount > 0 ? similarCount : undefined;
|
|
210
|
-
|
|
211
195
|
if (isSenderAddressUnverifiable(thread.fromEmail)) {
|
|
212
196
|
return {
|
|
213
197
|
verdict: "mismatch",
|
|
214
198
|
fromDomain: "",
|
|
215
199
|
addressUnreadable: true,
|
|
216
200
|
summary: UNREADABLE_SENDER_SUMMARY,
|
|
217
|
-
similarCount: surfacedSimilar,
|
|
218
201
|
};
|
|
219
202
|
}
|
|
220
203
|
|
|
@@ -268,35 +251,11 @@ export function buildAuthenticityIntel(
|
|
|
268
251
|
dkimDomain,
|
|
269
252
|
claimedBrand,
|
|
270
253
|
summary,
|
|
271
|
-
// Surface the lookalike count so the panel renders the "N similar
|
|
272
|
-
// messages" campaign-reveal button.
|
|
273
|
-
similarCount: surfacedSimilar,
|
|
274
254
|
};
|
|
275
255
|
}
|
|
276
256
|
|
|
277
257
|
export interface UseIntelligenceDataResult {
|
|
278
258
|
data: IntelligenceData | null;
|
|
279
|
-
/**
|
|
280
|
-
* True while similar-messages fetch is in flight. The rest of the data
|
|
281
|
-
* (sender, authenticity, category, flags) arrives synchronously from the
|
|
282
|
-
* already-loaded thread row.
|
|
283
|
-
*/
|
|
284
|
-
isSimilarLoading: boolean;
|
|
285
|
-
/** Non-null when the similar-messages fetch failed. */
|
|
286
|
-
similarError: unknown;
|
|
287
|
-
/**
|
|
288
|
-
* True when the similar-messages failure is a fatal first-party 5xx. The
|
|
289
|
-
* global escalation overlay handles it; the panel must NOT degrade it to the
|
|
290
|
-
* benign grey "Similarity search unavailable" label. A soft (non-fatal)
|
|
291
|
-
* error or an empty result keeps the panel quiet.
|
|
292
|
-
*/
|
|
293
|
-
similarErrorIsFatal: boolean;
|
|
294
|
-
/**
|
|
295
|
-
* False on an instance with semantic search off (#1068): nothing is embedded,
|
|
296
|
-
* so the query is never sent and the section states the setting instead of
|
|
297
|
-
* rendering an empty list. `undefined` until the config read answers.
|
|
298
|
-
*/
|
|
299
|
-
semanticEnabled: boolean | undefined;
|
|
300
259
|
/**
|
|
301
260
|
* True when the address lookup failed with a fatal first-party 5xx. The panel
|
|
302
261
|
* must not silently render the sender's flags (VIP/muted/blocked) as absent —
|
|
@@ -318,20 +277,16 @@ export interface UseIntelligenceDataResult {
|
|
|
318
277
|
|
|
319
278
|
/**
|
|
320
279
|
* Maps a `RemitImapThreadMessageResponse` into `IntelligenceData` for the
|
|
321
|
-
* `IntelligencePanel` component.
|
|
280
|
+
* `IntelligencePanel` component. Two data sources:
|
|
322
281
|
*
|
|
323
282
|
* 1. The thread row itself (already loaded) → sender basics, trust, authenticity, category.
|
|
324
283
|
* 2. `GET /addresses/search?q=<email>` → address flags (VIP/muted/blocked/etc.)
|
|
325
284
|
* and first-seen timestamp. One round-trip per unique sender.
|
|
326
|
-
* 3. `GET /search/semantic` → similar messages. Last to arrive; sidebar still
|
|
327
|
-
* renders if this fails.
|
|
328
285
|
*/
|
|
329
286
|
export function useIntelligenceData(
|
|
330
287
|
thread: RemitImapThreadMessageResponse | null | undefined,
|
|
331
|
-
currentMailboxId?: string,
|
|
332
288
|
): UseIntelligenceDataResult {
|
|
333
289
|
const senderEmail = thread?.fromEmail ?? null;
|
|
334
|
-
const subject = thread?.subject ?? "";
|
|
335
290
|
|
|
336
291
|
// --- Address lookup: flags + first-seen timestamp ---
|
|
337
292
|
const { data: addressSearchResult, error: addressError } = useQuery({
|
|
@@ -347,70 +302,22 @@ export function useIntelligenceData(
|
|
|
347
302
|
senderEmail ?? undefined,
|
|
348
303
|
);
|
|
349
304
|
|
|
350
|
-
// --- Semantic search: similar messages ---
|
|
351
|
-
const semanticEnabled = useSemanticSearchEnabled();
|
|
352
|
-
const semanticQuery = [subject, senderEmail].filter(Boolean).join(" ");
|
|
353
|
-
const {
|
|
354
|
-
data: semanticResult,
|
|
355
|
-
isLoading: isSimilarLoading,
|
|
356
|
-
error: similarError,
|
|
357
|
-
} = useQuery({
|
|
358
|
-
...semanticSearchOperationsSemanticSearchOptions({
|
|
359
|
-
query: { query: semanticQuery, limit: 5 },
|
|
360
|
-
}),
|
|
361
|
-
// Waits for the config read rather than racing it. An off instance answers
|
|
362
|
-
// this query with an empty list, which the panel cannot tell from a sender
|
|
363
|
-
// nothing resembles, so the request is not worth sending on the chance the
|
|
364
|
-
// instance turns out to be on. `undefined` is that read still in flight;
|
|
365
|
-
// it is cached process-wide and every other surface already made it.
|
|
366
|
-
enabled:
|
|
367
|
-
Boolean(thread && semanticQuery.length > 3) && semanticEnabled === true,
|
|
368
|
-
staleTime: 60_000,
|
|
369
|
-
// A non-fatal failure (e.g. 404, no index yet) is acceptable — the sidebar
|
|
370
|
-
// degrades gracefully. A 5xx is NOT acceptable: it escalates globally and
|
|
371
|
-
// the panel must not present it as a benign "unavailable" state.
|
|
372
|
-
retry: 1,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
305
|
const data = useMemo((): IntelligenceData | null => {
|
|
376
306
|
if (!thread) return null;
|
|
377
307
|
|
|
378
|
-
const similar: SimilarMessageIntel[] = (semanticResult?.items ?? [])
|
|
379
|
-
// Exclude the current message itself
|
|
380
|
-
.filter((r) => r.messageId !== thread.messageId)
|
|
381
|
-
.map((r) => ({
|
|
382
|
-
id: r.messageId,
|
|
383
|
-
threadId: r.threadId,
|
|
384
|
-
// Prefer the mailbox the user is already viewing when the message also
|
|
385
|
-
// lives there, so opening a similar message stays in the same folder;
|
|
386
|
-
// otherwise fall back to the first mailbox it lives in.
|
|
387
|
-
mailboxId:
|
|
388
|
-
currentMailboxId != null && r.mailboxIds.includes(currentMailboxId)
|
|
389
|
-
? currentMailboxId
|
|
390
|
-
: (r.mailboxIds[0] ?? ""),
|
|
391
|
-
fromName: r.fromName ?? "",
|
|
392
|
-
subject: r.subject ?? "(No subject)",
|
|
393
|
-
timeLabel: r.sentDate != null ? formatEmailDate(r.sentDate * 1000) : "",
|
|
394
|
-
matched: r.matchedChunkType as MatchedChunk,
|
|
395
|
-
}));
|
|
396
|
-
|
|
397
308
|
const sender = buildSenderIntel(thread, address);
|
|
398
309
|
|
|
399
|
-
const authenticity = buildAuthenticityIntel(thread
|
|
310
|
+
const authenticity = buildAuthenticityIntel(thread);
|
|
400
311
|
|
|
401
312
|
const category = buildCategoryIntel(thread, address);
|
|
402
313
|
|
|
403
314
|
const flags = buildSenderFlags(address);
|
|
404
315
|
|
|
405
|
-
return { sender, authenticity, category, flags
|
|
406
|
-
}, [thread, address
|
|
316
|
+
return { sender, authenticity, category, flags };
|
|
317
|
+
}, [thread, address]);
|
|
407
318
|
|
|
408
319
|
return {
|
|
409
320
|
data,
|
|
410
|
-
isSimilarLoading,
|
|
411
|
-
similarError,
|
|
412
|
-
similarErrorIsFatal: isServerError(similarError),
|
|
413
|
-
semanticEnabled,
|
|
414
321
|
addressErrorIsFatal: isServerError(addressError),
|
|
415
322
|
addressId: address?.addressId,
|
|
416
323
|
address,
|
|
@@ -93,52 +93,4 @@ describe("isSinglePaneTier — compose surface must mount below desktop", () =>
|
|
|
93
93
|
it("is NOT single-pane at desktop (reading pane hosts compose)", () => {
|
|
94
94
|
assert.equal(isSinglePaneTier("desktop"), false);
|
|
95
95
|
});
|
|
96
|
-
|
|
97
|
-
it("treats every below-desktop width as single-pane via resolveLayoutTier", () => {
|
|
98
|
-
assert.equal(isSinglePaneTier(resolveLayoutTier(mouse(390))), true);
|
|
99
|
-
assert.equal(isSinglePaneTier(resolveLayoutTier(mouse(768))), true);
|
|
100
|
-
assert.equal(isSinglePaneTier(resolveLayoutTier(mouse(1023))), true);
|
|
101
|
-
assert.equal(isSinglePaneTier(resolveLayoutTier(mouse(1024))), false);
|
|
102
|
-
assert.equal(
|
|
103
|
-
isSinglePaneTier(resolveLayoutTier(tabletHeldUpright(1024))),
|
|
104
|
-
true,
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("exactly one search field is mounted at every width", () => {
|
|
110
|
-
// Two call sites decide this from the same tier: `mail.tsx` mounts the top
|
|
111
|
-
// bar's field when the layout is not single-pane, and `MailListHeader`
|
|
112
|
-
// passes `showSearch` to keep the header's field otherwise. Both read
|
|
113
|
-
// `isSinglePaneTier`, so they are each other's complement by construction —
|
|
114
|
-
// if either is rewritten to its own expression this fails. Two mounted
|
|
115
|
-
// fields compete for "/" and for focus (#59); zero leaves no way to search.
|
|
116
|
-
const mountsTopBarField = (viewport: ViewportProfile): boolean =>
|
|
117
|
-
!isSinglePaneTier(resolveLayoutTier(viewport));
|
|
118
|
-
const mountsHeaderField = (viewport: ViewportProfile): boolean =>
|
|
119
|
-
isSinglePaneTier(resolveLayoutTier(viewport));
|
|
120
|
-
|
|
121
|
-
for (const viewport of [
|
|
122
|
-
mouse(0),
|
|
123
|
-
mouse(390),
|
|
124
|
-
mouse(767),
|
|
125
|
-
mouse(768),
|
|
126
|
-
mouse(1023),
|
|
127
|
-
mouse(1024),
|
|
128
|
-
mouse(1440),
|
|
129
|
-
tabletHeldUpright(1024),
|
|
130
|
-
]) {
|
|
131
|
-
it(`mounts one field at ${viewport.width}px ${viewport.orientation}/${viewport.pointer}`, () => {
|
|
132
|
-
const fields =
|
|
133
|
-
Number(mountsTopBarField(viewport)) +
|
|
134
|
-
Number(mountsHeaderField(viewport));
|
|
135
|
-
assert.equal(fields, 1, `${viewport.width}px mounts ${fields} fields`);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
it("puts the field in the top bar only from desktop up", () => {
|
|
140
|
-
assert.equal(mountsTopBarField(mouse(1023)), false);
|
|
141
|
-
assert.equal(mountsTopBarField(mouse(1024)), true);
|
|
142
|
-
assert.equal(mountsTopBarField(tabletHeldUpright(1024)), false);
|
|
143
|
-
});
|
|
144
96
|
});
|
|
@@ -54,7 +54,7 @@ export const GENERIC_SPAM_ACTION_FAILURE =
|
|
|
54
54
|
"This message could not be processed. Please try again. If it keeps failing, report it.";
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* The server's designed failure text names the message by
|
|
57
|
+
* The server's designed failure text names the message by putting its raw
|
|
58
58
|
* id as a possessive subject — e.g. "Message 4kv0xxyfhg4dhzqvxd105v840's move
|
|
59
59
|
* to Junk has not settled yet; try again in a moment." (message ids here are
|
|
60
60
|
* 25-char base36, `translator.generate()` in `packages/data-ports/src/id.ts`
|
|
@@ -139,16 +139,6 @@ describe("useRescueCandidates: candidate mapping from real hook output", () => {
|
|
|
139
139
|
senderTrust: "wellknown",
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
test("maps messageId to candidate id", () => {
|
|
143
|
-
const { candidates } = renderHookWithResponse({ items: [vipThread] });
|
|
144
|
-
assert.equal(candidates[0].id, "m1");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("maps fromName as senderName", () => {
|
|
148
|
-
const { candidates } = renderHookWithResponse({ items: [vipThread] });
|
|
149
|
-
assert.equal(candidates[0].senderName, "Anna de Vries");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
142
|
test("falls back to fromEmail when fromName is absent", () => {
|
|
153
143
|
const thread = makeThread({
|
|
154
144
|
messageId: "m3",
|
|
@@ -193,19 +183,6 @@ describe("useRescueCandidates: candidate mapping from real hook output", () => {
|
|
|
193
183
|
assert.equal(candidates[0].trustSubReason, "You've emailed them before");
|
|
194
184
|
});
|
|
195
185
|
|
|
196
|
-
test("sets the constant trustReason headline", () => {
|
|
197
|
-
const { candidates } = renderHookWithResponse({ items: [vipThread] });
|
|
198
|
-
assert.equal(candidates[0].trustReason, "We can verify this sender");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("preserves senderTrust on the candidate", () => {
|
|
202
|
-
const { candidates } = renderHookWithResponse({
|
|
203
|
-
items: [vipThread, wellknownThread],
|
|
204
|
-
});
|
|
205
|
-
assert.equal(candidates[0].senderTrust, "vip");
|
|
206
|
-
assert.equal(candidates[1].senderTrust, "wellknown");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
186
|
test("maps all response items to candidates in order", () => {
|
|
210
187
|
const { candidates } = renderHookWithResponse({
|
|
211
188
|
items: [vipThread, wellknownThread],
|
|
@@ -2,7 +2,10 @@ import { organizeOperationsPreviewOrganizeMutation } from "@remit/api-http-clien
|
|
|
2
2
|
import { type PreviewCount, UNCOUNTABLE_PREDICATE_REASON } from "@remit/ui";
|
|
3
3
|
import { useMutation } from "@tanstack/react-query";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildOrganizeInput,
|
|
7
|
+
type OrganizeMatchPredicate,
|
|
8
|
+
} from "@/lib/organize/organize-model";
|
|
6
9
|
import {
|
|
7
10
|
derivePreview,
|
|
8
11
|
isEvaluablePredicate,
|
|
@@ -10,13 +13,12 @@ import {
|
|
|
10
13
|
type PreviewState,
|
|
11
14
|
predicateSignature,
|
|
12
15
|
} from "@/lib/organize/rule-model";
|
|
13
|
-
import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* The live match count for the edited rule (RFC 038 D1). Every predicate change
|
|
17
19
|
* schedules a debounced `POST /organize/preview` and, until it lands, reports
|
|
18
20
|
* the last count as stale so the count never blanks and the commit gate can
|
|
19
|
-
* hold apply. Seeded with
|
|
21
|
+
* hold apply. Seeded with a count already known, so opening the editor shows a
|
|
20
22
|
* settled number without an immediate re-fetch.
|
|
21
23
|
*
|
|
22
24
|
* Out-of-order responses are dropped: only the reply to the most recent request
|
|
@@ -24,9 +26,9 @@ import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
|
|
|
24
26
|
* server bounds the match, so a broad predicate over a large mailbox stays a
|
|
25
27
|
* single cheap request; the debounce is what keeps rapid edits from spamming it.
|
|
26
28
|
*
|
|
27
|
-
* `seedCount` is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* `seedCount` is an opening count already known; omit it (as the settings
|
|
30
|
+
* editor does) to open on `loading` and preview the loaded predicate
|
|
31
|
+
* immediately.
|
|
30
32
|
*
|
|
31
33
|
* `matchedIds` are the ids that count was counted over — the server's own
|
|
32
34
|
* answer, bounded to the same cap the apply pass uses. A caller applying an
|
|
@@ -78,7 +80,6 @@ export const useRulePreview = (
|
|
|
78
80
|
setState({
|
|
79
81
|
count: response.matchedCount,
|
|
80
82
|
matchedIds: response.messageIds,
|
|
81
|
-
indexEmpty: response.semanticIndexEmpty === true,
|
|
82
83
|
previewedSignature: signature,
|
|
83
84
|
});
|
|
84
85
|
})
|
|
@@ -8,9 +8,8 @@ import { useBrowsedList } from "@/routing";
|
|
|
8
8
|
*
|
|
9
9
|
* `in:` is recognized only where the route carries no scope. On a scoped view
|
|
10
10
|
* the top bar's chip already answers "which mailbox", and a typed `in:` is a
|
|
11
|
-
* second, competing answer that no engine there acts on — the
|
|
12
|
-
*
|
|
13
|
-
* scope. Leaving the term unresolved means it stays ordinary words: it is
|
|
11
|
+
* second, competing answer that no engine there acts on — the search is pinned
|
|
12
|
+
* to the route's mailbox. Leaving the term unresolved means it stays ordinary words: it is
|
|
14
13
|
* matched as text and never chipped, so the field can't advertise a filter that
|
|
15
14
|
* does nothing. To search another folder, drop the scope chip or go to it.
|
|
16
15
|
*
|
|
@@ -25,12 +25,6 @@ import {
|
|
|
25
25
|
|
|
26
26
|
const NOW = 1_700_000_000_000;
|
|
27
27
|
|
|
28
|
-
describe("STALENESS_THRESHOLD_MS", () => {
|
|
29
|
-
test("equals 15 minutes in milliseconds", () => {
|
|
30
|
-
assert.equal(STALENESS_THRESHOLD_MS, 15 * 60 * 1000);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
28
|
describe("selectStaleAccountIds", () => {
|
|
35
29
|
test("treats accounts with undefined lastSyncAt as stale", () => {
|
|
36
30
|
const ids = selectStaleAccountIds(
|
|
@@ -92,10 +86,6 @@ describe("selectStaleAccountIds", () => {
|
|
|
92
86
|
assert.deepEqual(ids, ["a-1"]);
|
|
93
87
|
});
|
|
94
88
|
|
|
95
|
-
test("returns an empty list for an empty input", () => {
|
|
96
|
-
assert.deepEqual(selectStaleAccountIds([], NOW), []);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
89
|
test("equality on the threshold is treated as fresh", () => {
|
|
100
90
|
// `now - lastSyncAt > thresholdMs` — strict greater-than means an
|
|
101
91
|
// account synced exactly threshold ms ago is still fresh. Pinning
|
|
@@ -108,13 +98,6 @@ describe("selectStaleAccountIds", () => {
|
|
|
108
98
|
});
|
|
109
99
|
});
|
|
110
100
|
|
|
111
|
-
describe("POLL_INTERVAL_MS", () => {
|
|
112
|
-
// How stale an open tab's mail may be with nobody touching anything.
|
|
113
|
-
test("defaults to 30 seconds when mailboxPollIntervalSeconds is unset in this test run", () => {
|
|
114
|
-
assert.equal(POLL_INTERVAL_MS, 30 * 1000);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
101
|
describe("resolvePollIntervalMs", () => {
|
|
119
102
|
// This poll shares POST /sync with the refresh control, and that endpoint's
|
|
120
103
|
// triggers skip the server's per-mailbox freshness gate
|
|
@@ -96,16 +96,6 @@ function renderHook(
|
|
|
96
96
|
return captured;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
describe("starredThreadsQueryKey", () => {
|
|
100
|
-
test("asks for the starred listing, newest first", () => {
|
|
101
|
-
const [params] = starredThreadsQueryKey() as [
|
|
102
|
-
{ query?: { starred?: boolean; order?: string } },
|
|
103
|
-
];
|
|
104
|
-
assert.equal(params.query?.starred, true);
|
|
105
|
-
assert.equal(params.query?.order, "desc");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
99
|
describe("useStarredThreads", () => {
|
|
110
100
|
test("returns no threads when the listing is empty", () => {
|
|
111
101
|
assert.deepEqual(renderHook([{ items: [] }]), []);
|
|
@@ -15,10 +15,4 @@ describe("buildMailboxListKey", () => {
|
|
|
15
15
|
});
|
|
16
16
|
assert.deepEqual(got, want);
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
test("returns a different key for a different accountId", () => {
|
|
20
|
-
const a = buildMailboxListKey("acc-1");
|
|
21
|
-
const b = buildMailboxListKey("acc-2");
|
|
22
|
-
assert.notDeepEqual(a, b);
|
|
23
|
-
});
|
|
24
18
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { createDomHarness, type DomHarness } from "@/test-support/dom";
|
|
5
|
+
import { useVisualViewport } from "./useVisualViewport";
|
|
6
|
+
|
|
7
|
+
let harness: DomHarness | undefined;
|
|
8
|
+
|
|
9
|
+
function KeyboardProbe() {
|
|
10
|
+
const { isKeyboardOpen } = useVisualViewport();
|
|
11
|
+
return createElement("p", null, isKeyboardOpen ? "open" : "closed");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mount = (): {
|
|
15
|
+
dom: DomHarness;
|
|
16
|
+
fullHeight: number;
|
|
17
|
+
resize: (height: number) => void;
|
|
18
|
+
} => {
|
|
19
|
+
const fullHeight = window.innerHeight;
|
|
20
|
+
const viewport = Object.assign(new window.EventTarget(), {
|
|
21
|
+
height: fullHeight,
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(window, "visualViewport", {
|
|
24
|
+
configurable: true,
|
|
25
|
+
value: viewport,
|
|
26
|
+
});
|
|
27
|
+
const dom = createDomHarness();
|
|
28
|
+
harness = dom;
|
|
29
|
+
dom.render(createElement(KeyboardProbe));
|
|
30
|
+
return {
|
|
31
|
+
dom,
|
|
32
|
+
fullHeight,
|
|
33
|
+
resize: (height) => {
|
|
34
|
+
viewport.height = height;
|
|
35
|
+
dom.dispatch(viewport, new window.Event("resize"));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
harness?.close();
|
|
42
|
+
harness = undefined;
|
|
43
|
+
Reflect.deleteProperty(window, "visualViewport");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("the software keyboard", () => {
|
|
47
|
+
it("reads as open once the visual viewport shrinks past the threshold, and closed again when it restores", () => {
|
|
48
|
+
const { dom, fullHeight, resize } = mount();
|
|
49
|
+
assert.equal(dom.text(), "closed");
|
|
50
|
+
|
|
51
|
+
resize(fullHeight - 400);
|
|
52
|
+
assert.equal(dom.text(), "open");
|
|
53
|
+
|
|
54
|
+
resize(fullHeight);
|
|
55
|
+
assert.equal(dom.text(), "closed");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("stays closed when only the address bar collapses", () => {
|
|
59
|
+
const { dom, fullHeight, resize } = mount();
|
|
60
|
+
resize(fullHeight - 50);
|
|
61
|
+
assert.equal(dom.text(), "closed");
|
|
62
|
+
});
|
|
63
|
+
});
|