@remit/ui 0.0.57 → 0.0.59

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.
@@ -0,0 +1,673 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { RuleClause, RuleScope } from "../components/filter-rule.js";
4
+ import {
5
+ commitBlockedReason,
6
+ ruleBlockedCopy,
7
+ } from "../components/filter-rule.js";
8
+ import {
9
+ backExits,
10
+ clauseSentence,
11
+ clauseWords,
12
+ type MatchCount,
13
+ type MatchMode,
14
+ matchDoorHint,
15
+ matchDoorLabel,
16
+ matchPhrase,
17
+ matchSummary,
18
+ type RunState,
19
+ runCopy,
20
+ type StepId,
21
+ sampleEmptyCopy,
22
+ stepBlockedReason,
23
+ stepIndex,
24
+ stepLabel,
25
+ stepsFor,
26
+ unreadableDraftClauses,
27
+ type Verb,
28
+ verbCopy,
29
+ type WizardDraft,
30
+ } from "./wizard-steps.js";
31
+
32
+ const VERBS: Verb[] = ["delete", "move", "junk", "markRead", "organize"];
33
+ const MODES: MatchMode[] = ["selected", "similar", "properties"];
34
+ const SCOPES: (RuleScope | undefined)[] = [
35
+ undefined,
36
+ "once",
37
+ "standing",
38
+ "until",
39
+ ];
40
+
41
+ const clause = (
42
+ field: RuleClause["field"],
43
+ value: string,
44
+ id = field,
45
+ ): RuleClause => ({ id, field, value });
46
+
47
+ describe("stepsFor — the step ids for every verb × match mode × scope", () => {
48
+ it("gives the ticked door an opening step and no editor", () => {
49
+ assert.deepEqual(stepsFor({ verb: "delete", mode: "selected" }), [
50
+ "match",
51
+ "review",
52
+ "run",
53
+ ]);
54
+ assert.deepEqual(stepsFor({ verb: "junk", mode: "similar" }), [
55
+ "match",
56
+ "review",
57
+ "run",
58
+ ]);
59
+ assert.deepEqual(stepsFor({ verb: "markRead", mode: "properties" }), [
60
+ "match",
61
+ "properties",
62
+ "review",
63
+ "run",
64
+ ]);
65
+ });
66
+
67
+ it("gives Move a destination step", () => {
68
+ assert.deepEqual(stepsFor({ verb: "move", mode: "selected" }), [
69
+ "match",
70
+ "folder",
71
+ "review",
72
+ "run",
73
+ ]);
74
+ assert.deepEqual(stepsFor({ verb: "move", mode: "properties" }), [
75
+ "match",
76
+ "properties",
77
+ "folder",
78
+ "review",
79
+ "run",
80
+ ]);
81
+ });
82
+
83
+ it("gives Organize a scope step, and a naming step only where the scope persists", () => {
84
+ assert.deepEqual(stepsFor({ verb: "organize", mode: "selected" }), [
85
+ "match",
86
+ "rule",
87
+ "review",
88
+ "run",
89
+ ]);
90
+ assert.deepEqual(
91
+ stepsFor({ verb: "organize", mode: "selected", scope: "once" }),
92
+ ["match", "rule", "review", "run"],
93
+ );
94
+ assert.deepEqual(
95
+ stepsFor({ verb: "organize", mode: "similar", scope: "standing" }),
96
+ ["match", "rule", "name", "review", "run"],
97
+ );
98
+ assert.deepEqual(
99
+ stepsFor({ verb: "organize", mode: "properties", scope: "until" }),
100
+ ["match", "properties", "rule", "name", "review", "run"],
101
+ );
102
+ });
103
+
104
+ it("drops the match step when a search is what opened the wizard", () => {
105
+ assert.deepEqual(
106
+ stepsFor({
107
+ verb: "organize",
108
+ mode: "properties",
109
+ scope: "standing",
110
+ fromSearch: true,
111
+ }),
112
+ ["properties", "rule", "name", "review", "run"],
113
+ );
114
+ assert.deepEqual(
115
+ stepsFor({ verb: "move", mode: "properties", fromSearch: true }),
116
+ ["properties", "folder", "review", "run"],
117
+ );
118
+ });
119
+
120
+ it("always opens on a step and always ends on the run", () => {
121
+ for (const verb of VERBS) {
122
+ for (const mode of MODES) {
123
+ for (const scope of SCOPES) {
124
+ for (const fromSearch of [false, true]) {
125
+ const steps = stepsFor({ verb, mode, scope, fromSearch });
126
+ assert.ok(steps.length >= 3, `${verb}/${mode}/${scope}`);
127
+ assert.equal(steps.at(-1), "run");
128
+ assert.equal(steps.at(-2), "review");
129
+ assert.equal(
130
+ new Set(steps).size,
131
+ steps.length,
132
+ "no step appears twice",
133
+ );
134
+ }
135
+ }
136
+ }
137
+ }
138
+ });
139
+ });
140
+
141
+ describe("stepsFor — a branching answer only adds or drops a step after itself", () => {
142
+ const positionOf = (steps: StepId[], step: StepId): number =>
143
+ steps.indexOf(step);
144
+
145
+ it("keeps the match step and everything before the answer in place", () => {
146
+ for (const verb of VERBS) {
147
+ for (const scope of SCOPES) {
148
+ const lists = MODES.map((mode) => stepsFor({ verb, mode, scope }));
149
+ for (const steps of lists) {
150
+ assert.equal(positionOf(steps, "match"), 0);
151
+ }
152
+ }
153
+ }
154
+ });
155
+
156
+ it("keeps the scope step in place across every scope answer", () => {
157
+ for (const mode of MODES) {
158
+ const lists = SCOPES.map((scope) =>
159
+ stepsFor({ verb: "organize", mode, scope }),
160
+ );
161
+ const positions = new Set(
162
+ lists.map((steps) => positionOf(steps, "rule")),
163
+ );
164
+ assert.equal(positions.size, 1, "the rule step never moves");
165
+ }
166
+ });
167
+
168
+ it("keeps the user on the step they were looking at when the list shortens", () => {
169
+ const standing = stepsFor({
170
+ verb: "organize",
171
+ mode: "selected",
172
+ scope: "standing",
173
+ });
174
+ assert.deepEqual(standing, ["match", "rule", "name", "review", "run"]);
175
+
176
+ const once = stepsFor({
177
+ verb: "organize",
178
+ mode: "selected",
179
+ scope: "once",
180
+ });
181
+ assert.deepEqual(once, ["match", "rule", "review", "run"]);
182
+
183
+ // The user is on Review when they go back and change the scope to once.
184
+ // Held by number, index 3 lands them on Run — the action taken without the
185
+ // review screen they were standing on. Held by id, Review is still Review.
186
+ assert.equal(once[standing.indexOf("review")], "run");
187
+ assert.equal(once[stepIndex(once, "review")], "review");
188
+
189
+ // And the step the answer dropped resolves to the opening step rather than
190
+ // to whatever now sits at its old number.
191
+ assert.equal(once[standing.indexOf("name")], "review");
192
+ assert.equal(once[stepIndex(once, "name")], "match");
193
+
194
+ // A step both lists hold stays the same step, whichever way the answer went.
195
+ for (const held of ["match", "rule", "review", "run"] as StepId[]) {
196
+ assert.equal(once[stepIndex(once, held)], held);
197
+ assert.equal(standing[stepIndex(standing, held)], held);
198
+ }
199
+ });
200
+
201
+ it("keeps the properties step from stranding when the door changes", () => {
202
+ const withEditor = stepsFor({ verb: "move", mode: "properties" });
203
+ const withoutEditor = stepsFor({ verb: "move", mode: "selected" });
204
+ assert.deepEqual(withEditor, [
205
+ "match",
206
+ "properties",
207
+ "folder",
208
+ "review",
209
+ "run",
210
+ ]);
211
+ assert.deepEqual(withoutEditor, ["match", "folder", "review", "run"]);
212
+
213
+ const heldByNumber = withEditor.indexOf("properties");
214
+ assert.equal(withoutEditor[heldByNumber], "folder");
215
+ assert.equal(
216
+ withoutEditor[stepIndex(withoutEditor, "properties")],
217
+ "match",
218
+ );
219
+ });
220
+
221
+ it("resolves a step the answers dropped back to the opening step", () => {
222
+ const shortened = stepsFor({ verb: "organize", mode: "selected" });
223
+ assert.equal(shortened.includes("properties"), false);
224
+ assert.equal(stepIndex(shortened, "properties"), 0);
225
+ assert.equal(stepIndex(shortened, "rule"), 1);
226
+ });
227
+ });
228
+
229
+ describe("backExits", () => {
230
+ it("leaves the wizard from the opening step, which has nothing behind it", () => {
231
+ const steps = stepsFor({ verb: "move", mode: "selected" });
232
+ assert.equal(backExits(steps, "match"), true);
233
+ assert.equal(backExits(steps, "folder"), false);
234
+ assert.equal(backExits(steps, "review"), false);
235
+ });
236
+
237
+ it("leaves the wizard from the run, whose action has already happened", () => {
238
+ const steps = stepsFor({ verb: "move", mode: "selected" });
239
+ assert.equal(backExits(steps, "run"), true);
240
+ });
241
+
242
+ it("leaves the wizard from the properties step a search opened on", () => {
243
+ const steps = stepsFor({
244
+ verb: "organize",
245
+ mode: "properties",
246
+ fromSearch: true,
247
+ });
248
+ assert.equal(backExits(steps, "properties"), true);
249
+ assert.equal(backExits(steps, "rule"), false);
250
+ });
251
+ });
252
+
253
+ const UNCOUNTED: MatchCount = { status: "uncounted" };
254
+
255
+ describe("stepBlockedReason", () => {
256
+ const draft = (over: Partial<WizardDraft> = {}): WizardDraft => ({
257
+ clauses: [],
258
+ matchOperator: "all",
259
+ ...over,
260
+ });
261
+
262
+ it("names what the properties step is missing, in the rule editor's words", () => {
263
+ assert.equal(
264
+ stepBlockedReason("properties", draft(), UNCOUNTED),
265
+ ruleBlockedCopy.noMatch,
266
+ );
267
+ assert.equal(
268
+ stepBlockedReason(
269
+ "properties",
270
+ draft({ clauses: [clause("From", " ")] }),
271
+ UNCOUNTED,
272
+ ),
273
+ "Fill in every property, or take the empty one off.",
274
+ );
275
+ assert.equal(
276
+ stepBlockedReason(
277
+ "properties",
278
+ draft({ clauses: [clause("From", "a@b.example")] }),
279
+ UNCOUNTED,
280
+ ),
281
+ undefined,
282
+ );
283
+ });
284
+
285
+ it("names what the folder step is missing", () => {
286
+ assert.equal(
287
+ stepBlockedReason("folder", draft(), UNCOUNTED),
288
+ "Pick a destination first.",
289
+ );
290
+ assert.equal(
291
+ stepBlockedReason(
292
+ "folder",
293
+ draft({ moveMailboxId: "mbx-travel" }),
294
+ UNCOUNTED,
295
+ ),
296
+ undefined,
297
+ );
298
+ });
299
+
300
+ it("says a one-time apply cannot read message bodies, where the scope is chosen", () => {
301
+ const clauses = [clause("HasWords", "boarding pass")];
302
+ assert.equal(
303
+ stepBlockedReason("rule", draft({ clauses, scope: "once" }), UNCOUNTED),
304
+ ruleBlockedCopy.bodyTextOnce,
305
+ );
306
+ assert.equal(
307
+ stepBlockedReason(
308
+ "rule",
309
+ draft({ clauses, scope: "standing" }),
310
+ UNCOUNTED,
311
+ ),
312
+ undefined,
313
+ );
314
+ });
315
+
316
+ it("names what the scope step is missing", () => {
317
+ assert.equal(
318
+ stepBlockedReason("rule", draft(), UNCOUNTED),
319
+ "Choose one of the three first.",
320
+ );
321
+ assert.equal(
322
+ stepBlockedReason("rule", draft({ scope: "until" }), UNCOUNTED),
323
+ ruleBlockedCopy.noUntilDate,
324
+ );
325
+ assert.equal(
326
+ stepBlockedReason(
327
+ "rule",
328
+ draft({ scope: "until", until: "2026-09-01" }),
329
+ UNCOUNTED,
330
+ ),
331
+ undefined,
332
+ );
333
+ assert.equal(
334
+ stepBlockedReason("rule", draft({ scope: "once" }), UNCOUNTED),
335
+ undefined,
336
+ );
337
+ });
338
+
339
+ it("names what the naming step is missing", () => {
340
+ assert.equal(
341
+ stepBlockedReason("name", draft(), UNCOUNTED),
342
+ ruleBlockedCopy.unnamed,
343
+ );
344
+ assert.equal(
345
+ stepBlockedReason("name", draft({ name: " " }), UNCOUNTED),
346
+ ruleBlockedCopy.unnamed,
347
+ );
348
+ assert.equal(
349
+ stepBlockedReason("name", draft({ name: "Receipts" }), UNCOUNTED),
350
+ undefined,
351
+ );
352
+ });
353
+
354
+ it("holds the commit until the server's count has settled", () => {
355
+ assert.equal(
356
+ stepBlockedReason("review", draft(), { status: "loading" }),
357
+ ruleBlockedCopy.counting,
358
+ );
359
+ assert.equal(
360
+ stepBlockedReason("review", draft(), {
361
+ status: "ready",
362
+ count: 4,
363
+ stale: true,
364
+ }),
365
+ ruleBlockedCopy.recounting,
366
+ );
367
+ assert.equal(
368
+ stepBlockedReason("review", draft(), { status: "ready", count: 4 }),
369
+ undefined,
370
+ );
371
+ });
372
+
373
+ it("waits on no count where a widened door never had one", () => {
374
+ assert.equal(stepBlockedReason("review", draft(), UNCOUNTED), undefined);
375
+ });
376
+
377
+ it("lets the semantic widen carry a clause the literal matcher cannot read", () => {
378
+ // The clause was added on the property door, then the user went back and
379
+ // took "Similar to these". The widen is the matcher now and it reads
380
+ // bodies, so a one-off apply is fine — and the property step is not even in
381
+ // this step list, so a block here would name a clause on no reachable
382
+ // screen.
383
+ const clauses = [clause("HasWords", "boarding pass")];
384
+ const similar = draft({
385
+ clauses,
386
+ scope: "once",
387
+ widen: { anchorCount: 3 },
388
+ });
389
+ assert.equal(
390
+ stepsFor({ verb: "organize", mode: "similar", scope: "once" }).includes(
391
+ "properties",
392
+ ),
393
+ false,
394
+ );
395
+ assert.deepEqual(unreadableDraftClauses(similar), []);
396
+ assert.equal(stepBlockedReason("rule", similar, UNCOUNTED), undefined);
397
+ });
398
+
399
+ it("holds a one-off apply again once the widen cannot be evaluated", () => {
400
+ const blocked = draft({
401
+ clauses: [clause("HasWords", "boarding pass")],
402
+ scope: "once",
403
+ widen: { anchorCount: 3, inactive: true },
404
+ });
405
+ assert.equal(
406
+ stepBlockedReason("rule", blocked, UNCOUNTED),
407
+ ruleBlockedCopy.bodyTextOnce,
408
+ );
409
+ });
410
+
411
+ it("blocks nothing on the steps that ask nothing", () => {
412
+ for (const step of ["match", "run"] as StepId[]) {
413
+ assert.equal(stepBlockedReason(step, draft(), UNCOUNTED), undefined);
414
+ }
415
+ });
416
+
417
+ it("says the same thing the rule editor says about the same gap", () => {
418
+ const rule = {
419
+ clauses: [],
420
+ matchOperator: "all" as const,
421
+ scope: "standing" as const,
422
+ };
423
+ assert.equal(
424
+ commitBlockedReason(rule, { status: "ready", count: 0 }),
425
+ stepBlockedReason("properties", draft(), UNCOUNTED),
426
+ );
427
+ });
428
+ });
429
+
430
+ describe("runCopy", () => {
431
+ const outcome = (state: RunState, scope?: RuleScope) =>
432
+ runCopy({
433
+ state,
434
+ verb: "move",
435
+ scope,
436
+ matched: 12,
437
+ applied: 10,
438
+ failed: 2,
439
+ });
440
+
441
+ it("always offers a way out", () => {
442
+ const states: RunState[] = [
443
+ "saving",
444
+ "backApplyRunning",
445
+ "backApplyComplete",
446
+ "backApplyFailed",
447
+ "backApplyStartFailed",
448
+ "filterSaved",
449
+ "commitFailed",
450
+ ];
451
+ for (const state of states) {
452
+ for (const scope of SCOPES) {
453
+ const copy = outcome(state, scope);
454
+ assert.notEqual(copy.dismissLabel, "");
455
+ assert.notEqual(copy.title, "");
456
+ assert.notEqual(copy.detail, "");
457
+ }
458
+ }
459
+ });
460
+
461
+ it("says the rule survived a back-apply that did not", () => {
462
+ const standing = outcome("backApplyFailed", "standing");
463
+ assert.match(standing.title, /Rule saved/);
464
+ assert.match(standing.detail, /The rule itself is saved/);
465
+ assert.equal(standing.retryLabel, "Retry 2");
466
+ assert.equal(standing.tone, "warning");
467
+
468
+ const once = outcome("backApplyFailed", "once");
469
+ assert.equal(once.title, "Not everything was moved");
470
+ assert.doesNotMatch(once.detail, /rule/);
471
+ });
472
+
473
+ it("ends a one-off run on its own count and a saved rule on the rule", () => {
474
+ assert.equal(outcome("backApplyComplete", "once").title, "Moved 10");
475
+ assert.equal(
476
+ outcome("backApplyComplete", "standing").title,
477
+ "Rule saved and applied",
478
+ );
479
+ assert.match(
480
+ outcome("backApplyComplete", "standing").detail,
481
+ /New mail follows the rule as it arrives/,
482
+ );
483
+ });
484
+
485
+ it("separates a back-apply that never started from one that failed", () => {
486
+ const started = outcome("backApplyStartFailed", "standing");
487
+ assert.equal(started.title, "Rule saved");
488
+ assert.equal(started.retryLabel, "Run it over existing mail");
489
+ assert.equal(started.showProgress, false);
490
+ });
491
+
492
+ it("says a filter saved with nothing to back-apply is still live", () => {
493
+ const saved = outcome("filterSaved", "standing");
494
+ assert.equal(saved.title, "Filter saved");
495
+ assert.match(saved.detail, /New mail follows it as it arrives/);
496
+ assert.equal(saved.retryLabel, undefined);
497
+ });
498
+
499
+ it("says nothing changed when the create itself failed", () => {
500
+ const failed = outcome("commitFailed", "standing");
501
+ assert.equal(failed.title, "Couldn't save the rule");
502
+ assert.equal(failed.tone, "danger");
503
+ assert.equal(failed.retryLabel, "Try again");
504
+ assert.equal(outcome("commitFailed", "once").title, "Couldn't start move");
505
+ });
506
+
507
+ it("shows progress only while a pass over existing mail is under way or finished", () => {
508
+ assert.equal(outcome("saving", "standing").showProgress, false);
509
+ assert.equal(outcome("backApplyRunning", "standing").showProgress, true);
510
+ assert.equal(outcome("backApplyComplete", "once").showProgress, true);
511
+ assert.equal(outcome("backApplyFailed", "once").showProgress, true);
512
+ });
513
+
514
+ it("names the failure list in the verb's own past tense", () => {
515
+ assert.equal(
516
+ runCopy({
517
+ state: "backApplyFailed",
518
+ verb: "delete",
519
+ matched: 3,
520
+ applied: 1,
521
+ failed: 2,
522
+ }).failureListLabel,
523
+ "Not deleted",
524
+ );
525
+ });
526
+
527
+ it("counts the messages while a one-off run is in flight", () => {
528
+ assert.equal(
529
+ outcome("backApplyRunning", "once").title,
530
+ "Moving 12 messages…",
531
+ );
532
+ assert.equal(outcome("saving", "once").title, "Applying…");
533
+ assert.equal(outcome("saving", "standing").title, "Saving rule…");
534
+ });
535
+
536
+ it("heads the screen Done only once something finished", () => {
537
+ assert.equal(outcome("saving", "standing").screenTitle, "Move");
538
+ assert.equal(outcome("backApplyRunning", "standing").screenTitle, "Move");
539
+ // Nothing was created, so nothing was done.
540
+ assert.equal(outcome("commitFailed", "standing").screenTitle, "Move");
541
+
542
+ for (const state of [
543
+ "backApplyComplete",
544
+ "backApplyFailed",
545
+ "backApplyStartFailed",
546
+ "filterSaved",
547
+ ] as RunState[]) {
548
+ assert.equal(outcome(state, "standing").screenTitle, "Done", state);
549
+ }
550
+ });
551
+ });
552
+
553
+ describe("sample and door vocabulary", () => {
554
+ it("explains an empty sample rather than leaving it bare", () => {
555
+ assert.match(sampleEmptyCopy("noMatch"), /Nothing matches/);
556
+ assert.match(sampleEmptyCopy("notIndexed"), /isn't indexed yet/);
557
+ });
558
+
559
+ it("names each door and what it does", () => {
560
+ assert.equal(matchDoorLabel("selected", 3), "These 3 messages");
561
+ assert.equal(matchDoorLabel("similar", 3), "Similar to these 3");
562
+ assert.equal(matchDoorLabel("properties", 3), "Its properties");
563
+ assert.match(matchDoorHint("selected"), /ticked in the list/);
564
+ assert.match(matchDoorHint("properties"), /No reading involved/);
565
+ });
566
+
567
+ it("labels every step", () => {
568
+ assert.equal(stepLabel("match"), "Apply to");
569
+ assert.equal(stepLabel("run"), "Run");
570
+ });
571
+
572
+ it("carries a present and past tense for every verb", () => {
573
+ for (const verb of VERBS) {
574
+ const copy = verbCopy(verb);
575
+ assert.notEqual(copy.label, "");
576
+ assert.notEqual(copy.present, "");
577
+ assert.notEqual(copy.past, "");
578
+ }
579
+ assert.equal(verbCopy("delete").destructive, true);
580
+ assert.equal(verbCopy("move").destructive, false);
581
+ });
582
+ });
583
+
584
+ describe("the match as words", () => {
585
+ const clauses = [
586
+ clause("From", "noreply@booking.com"),
587
+ clause("Subject", "trip"),
588
+ ];
589
+
590
+ it("reads one clause as a field and a value", () => {
591
+ assert.equal(clauseWords(clauses[0]), 'From "noreply@booking.com"');
592
+ });
593
+
594
+ it("joins clauses with the word the operator reads as", () => {
595
+ assert.equal(
596
+ clauseSentence(clauses, "all"),
597
+ 'From "noreply@booking.com" and Subject "trip"',
598
+ );
599
+ assert.equal(
600
+ clauseSentence(clauses, "any"),
601
+ 'From "noreply@booking.com" or Subject "trip"',
602
+ );
603
+ });
604
+
605
+ it("summarises each door for the review list", () => {
606
+ assert.equal(
607
+ matchSummary({
608
+ mode: "selected",
609
+ selectedCount: 4,
610
+ clauses,
611
+ matchOperator: "all",
612
+ }),
613
+ "These 4 messages",
614
+ );
615
+ assert.equal(
616
+ matchSummary({
617
+ mode: "similar",
618
+ selectedCount: 4,
619
+ clauses,
620
+ matchOperator: "all",
621
+ }),
622
+ "Similar to these 4",
623
+ );
624
+ assert.equal(
625
+ matchSummary({
626
+ mode: "properties",
627
+ selectedCount: 4,
628
+ clauses,
629
+ matchOperator: "any",
630
+ }),
631
+ 'From "noreply@booking.com" or Subject "trip"',
632
+ );
633
+ });
634
+
635
+ it("phrases each door for the review sentence", () => {
636
+ assert.equal(
637
+ matchPhrase({
638
+ mode: "selected",
639
+ selectedCount: 4,
640
+ clauses,
641
+ matchOperator: "all",
642
+ }),
643
+ "4 messages",
644
+ );
645
+ assert.equal(
646
+ matchPhrase({
647
+ mode: "similar",
648
+ selectedCount: 4,
649
+ clauses,
650
+ matchOperator: "all",
651
+ }),
652
+ "mail similar to these 4",
653
+ );
654
+ assert.equal(
655
+ matchPhrase({
656
+ mode: "properties",
657
+ selectedCount: 4,
658
+ clauses,
659
+ matchOperator: "all",
660
+ }),
661
+ 'every message where From "noreply@booking.com" and Subject "trip"',
662
+ );
663
+ assert.equal(
664
+ matchPhrase({
665
+ mode: "properties",
666
+ selectedCount: 0,
667
+ clauses: [],
668
+ matchOperator: "all",
669
+ }),
670
+ "every message",
671
+ );
672
+ });
673
+ });