@@ -270,9 +270,44 @@ void main() {
270
270
check (view.results).single.which (
271
271
isUnicodeResult (names: ['smile' ]));
272
272
});
273
+
274
+ Future <Iterable <EmojiAutocompleteResult >> resultsOf (
275
+ String query, {
276
+ Map <String , String > realmEmoji = const {},
277
+ Map <String , List <String >>? unicodeEmoji,
278
+ }) async {
279
+ final store = prepare (realmEmoji: realmEmoji, unicodeEmoji: unicodeEmoji);
280
+ final view = EmojiAutocompleteView .init (store: store,
281
+ query: EmojiAutocompleteQuery (query));
282
+ bool done = false ;
283
+ view.addListener (() { done = true ; });
284
+ await Future (() {});
285
+ check (done).isTrue ();
286
+ return view.results;
287
+ }
288
+
289
+ test ('results end-to-end' , () async {
290
+ final unicodeEmoji = {
291
+ '1f4d3' : ['notebook' ], '1f516' : ['bookmark' ], '1f4d6' : ['book' ]};
292
+
293
+ // Empty query -> base ordering.
294
+ check (await resultsOf ('' , unicodeEmoji: unicodeEmoji)).deepEquals ([
295
+ isUnicodeResult (names: ['notebook' ]),
296
+ isUnicodeResult (names: ['bookmark' ]),
297
+ isUnicodeResult (names: ['book' ]),
298
+ isZulipResult (),
299
+ ]);
300
+
301
+ // With query, exact match precedes prefix match precedes other.
302
+ check (await resultsOf ('book' , unicodeEmoji: unicodeEmoji)).deepEquals ([
303
+ isUnicodeResult (names: ['book' ]),
304
+ isUnicodeResult (names: ['bookmark' ]),
305
+ isUnicodeResult (names: ['notebook' ]),
306
+ ]);
307
+ });
273
308
});
274
309
275
- group ('EmojiAutocompleteQuery.match ' , () {
310
+ group ('EmojiAutocompleteQuery' , () {
276
311
EmojiCandidate unicode (List <String > names, {String ? emojiCode}) {
277
312
emojiCode ?? = '10ffff' ;
278
313
return EmojiCandidate (emojiType: ReactionType .unicodeEmoji,
@@ -296,63 +331,71 @@ void main() {
296
331
}
297
332
298
333
test ('one-word query matches anywhere in name' , () {
299
- check (matchOfName ('' , 'smile' )).match ;
300
- check (matchOfName ('s' , 'smile' )).match ;
301
- check (matchOfName ('sm' , 'smile' )).match ;
302
- check (matchOfName ('smile' , 'smile' )).match ;
303
- check (matchOfName ('m' , 'smile' )).match ;
304
- check (matchOfName ('mile' , 'smile' )).match ;
305
- check (matchOfName ('e' , 'smile' )).match ;
334
+ check (matchOfName ('' , 'smile' )).prefix ;
335
+ check (matchOfName ('s' , 'smile' )).prefix ;
336
+ check (matchOfName ('sm' , 'smile' )).prefix ;
337
+ check (matchOfName ('smile' , 'smile' )).exact ;
338
+ check (matchOfName ('m' , 'smile' )).other ;
339
+ check (matchOfName ('mile' , 'smile' )).other ;
340
+ check (matchOfName ('e' , 'smile' )).other ;
306
341
307
342
check (matchOfName ('smiley' , 'smile' )).none;
308
343
check (matchOfName ('a' , 'smile' )).none;
309
344
310
- check (matchOfName ('o' , 'open_book' )).match ;
311
- check (matchOfName ('open' , 'open_book' )).match ;
312
- check (matchOfName ('pe' , 'open_book' )).match ;
313
- check (matchOfName ('boo' , 'open_book' )).match ;
314
- check (matchOfName ('ok' , 'open_book' )).match ;
345
+ check (matchOfName ('o' , 'open_book' )).prefix ;
346
+ check (matchOfName ('open' , 'open_book' )).prefix ;
347
+ check (matchOfName ('pe' , 'open_book' )).other ;
348
+ check (matchOfName ('boo' , 'open_book' )).other ;
349
+ check (matchOfName ('ok' , 'open_book' )).other ;
315
350
});
316
351
317
352
test ('multi-word query matches from start of a word' , () {
318
- check (matchOfName ('open_' , 'open_book' )).match ;
319
- check (matchOfName ('open_b' , 'open_book' )).match ;
320
- check (matchOfName ('open_book' , 'open_book' )).match ;
353
+ check (matchOfName ('open_' , 'open_book' )).prefix ;
354
+ check (matchOfName ('open_b' , 'open_book' )).prefix ;
355
+ check (matchOfName ('open_book' , 'open_book' )).exact ;
321
356
322
357
check (matchOfName ('pen_' , 'open_book' )).none;
323
358
check (matchOfName ('n_b' , 'open_book' )).none;
324
359
325
- check (matchOfName ('blue_dia' , 'large_blue_diamond' )).match ;
360
+ check (matchOfName ('blue_dia' , 'large_blue_diamond' )).other ;
326
361
});
327
362
328
363
test ('spaces in query behave as underscores' , () {
329
- check (matchOfName ('open ' , 'open_book' )).match ;
330
- check (matchOfName ('open b' , 'open_book' )).match ;
331
- check (matchOfName ('open book' , 'open_book' )).match ;
364
+ check (matchOfName ('open ' , 'open_book' )).prefix ;
365
+ check (matchOfName ('open b' , 'open_book' )).prefix ;
366
+ check (matchOfName ('open book' , 'open_book' )).exact ;
332
367
333
368
check (matchOfName ('pen ' , 'open_book' )).none;
334
369
check (matchOfName ('n b' , 'open_book' )).none;
335
370
336
- check (matchOfName ('blue dia' , 'large_blue_diamond' )).match ;
371
+ check (matchOfName ('blue dia' , 'large_blue_diamond' )).other ;
337
372
});
338
373
339
374
test ('query is lower-cased' , () {
340
- check (matchOfName ('Smi' , 'smile' )).match ;
375
+ check (matchOfName ('Smi' , 'smile' )).prefix ;
341
376
});
342
377
343
378
test ('query matches aliases same way as primary name' , () {
344
- check (matchOfNames ('a' , ['a' , 'b' ])).match ;
345
- check (matchOfNames ('b' , ['a' , 'b' ])).match ;
379
+ check (matchOfNames ('a' , ['a' , 'b' ])).exact ;
380
+ check (matchOfNames ('b' , ['a' , 'b' ])).exact ;
346
381
check (matchOfNames ('c' , ['a' , 'b' ])).none;
347
382
348
- check (matchOfNames ('pe' , ['x' , 'open_book' ])).match ;
349
- check (matchOfNames ('ok' , ['x' , 'open_book' ])).match ;
383
+ check (matchOfNames ('pe' , ['x' , 'open_book' ])).other ;
384
+ check (matchOfNames ('ok' , ['x' , 'open_book' ])).other ;
350
385
351
- check (matchOfNames ('open_' , ['x' , 'open_book' ])).match ;
352
- check (matchOfNames ('open b' , ['x' , 'open_book' ])).match ;
386
+ check (matchOfNames ('open_' , ['x' , 'open_book' ])).prefix ;
387
+ check (matchOfNames ('open b' , ['x' , 'open_book' ])).prefix ;
353
388
check (matchOfNames ('pen_' , ['x' , 'open_book' ])).none;
354
389
355
- check (matchOfNames ('Smi' , ['x' , 'smile' ])).match;
390
+ check (matchOfNames ('Smi' , ['x' , 'smile' ])).prefix;
391
+ });
392
+
393
+ test ('best match among name and aliases prevails' , () {
394
+ check (matchOfNames ('a' , ['ab' , 'a' , 'ba' , 'x' ])).exact;
395
+ check (matchOfNames ('a' , ['ba' , 'ab' , 'x' ])).prefix;
396
+ check (matchOfNames ('a' , ['ba' , 'ab' ])).prefix;
397
+ check (matchOfNames ('a' , ['ba' , 'x' ])).other;
398
+ check (matchOfNames ('a' , ['x' , 'y' , 'z' ])).none;
356
399
});
357
400
358
401
test ('query matches literal Unicode value' , () {
@@ -366,13 +409,13 @@ void main() {
366
409
check (matchOfLiteral ('1f642' , aka: '1f642' , '1f642' )).none;
367
410
368
411
// Matching the Unicode value the code describes does count…
369
- check (matchOfLiteral ('🙂' , aka: '\u {1f642}' , '1f642' )).match ;
412
+ check (matchOfLiteral ('🙂' , aka: '\u {1f642}' , '1f642' )).exact ;
370
413
// … and failing to match it doesn't make a match.
371
414
check (matchOfLiteral ('🙁' , aka: '\u {1f641}' , '1f642' )).none;
372
415
373
416
// Multi-code-point emoji work fine.
374
417
check (matchOfLiteral ('🏳🌈' , aka: '\u {1f3f3}\u {200d}\u {1f308}' ,
375
- '1f3f3-200d-1f308' )).match ;
418
+ '1f3f3-200d-1f308' )).exact ;
376
419
// Only exact matches count; no partial matches.
377
420
check (matchOfLiteral ('🏳' , aka: '\u {1f3f3}' ,
378
421
'1f3f3-200d-1f308' )).none;
@@ -393,11 +436,11 @@ void main() {
393
436
resolvedStillUrl: eg.realmUrl.resolve ('/emoji/1-still.png' )));
394
437
}
395
438
396
- check (matchOf ('eqeq' , realmCandidate ('eqeq' ))).match ;
397
- check (matchOf ('open_' , realmCandidate ('open_book' ))).match ;
439
+ check (matchOf ('eqeq' , realmCandidate ('eqeq' ))).exact ;
440
+ check (matchOf ('open_' , realmCandidate ('open_book' ))).prefix ;
398
441
check (matchOf ('n_b' , realmCandidate ('open_book' ))).none;
399
- check (matchOf ('blue dia' , realmCandidate ('large_blue_diamond' ))).match ;
400
- check (matchOf ('Smi' , realmCandidate ('smile' ))).match ;
442
+ check (matchOf ('blue dia' , realmCandidate ('large_blue_diamond' ))).other ;
443
+ check (matchOf ('Smi' , realmCandidate ('smile' ))).prefix ;
401
444
});
402
445
403
446
test ('can match Zulip extra emoji' , () {
@@ -409,11 +452,32 @@ void main() {
409
452
emojiType: ReactionType .zulipExtraEmoji,
410
453
emojiCode: 'zulip' , emojiName: 'zulip' ));
411
454
412
- check (matchOf ('z' , zulipCandidate)).match ;
413
- check (matchOf ('Zulip' , zulipCandidate)).match ;
414
- check (matchOf ('p' , zulipCandidate)).match ;
455
+ check (matchOf ('z' , zulipCandidate)).prefix ;
456
+ check (matchOf ('Zulip' , zulipCandidate)).exact ;
457
+ check (matchOf ('p' , zulipCandidate)).other ;
415
458
check (matchOf ('x' , zulipCandidate)).none;
416
459
});
460
+
461
+ int ? rankOf (String query, EmojiCandidate candidate) {
462
+ return EmojiAutocompleteQuery (query).testCandidate (candidate)? .rank;
463
+ }
464
+
465
+ void checkPrecedes (String query, EmojiCandidate a, EmojiCandidate b) {
466
+ check (rankOf (query, a)! ).isLessThan (rankOf (query, b)! );
467
+ }
468
+
469
+ test ('ranks exact before prefix before other match' , () {
470
+ checkPrecedes ('o' , unicode (['o' ]), unicode (['onion' ]));
471
+ checkPrecedes ('o' , unicode (['onion' ]), unicode (['book' ]));
472
+ });
473
+
474
+ test ('full list of ranks' , () {
475
+ check ([
476
+ rankOf ('o' , unicode (['o' ])), // exact
477
+ rankOf ('o' , unicode (['onion' ])), // prefix
478
+ rankOf ('o' , unicode (['book' ])), // other
479
+ ]).deepEquals ([0 , 1 , 2 ]);
480
+ });
417
481
});
418
482
}
419
483
@@ -439,7 +503,9 @@ extension EmojiCandidateChecks on Subject<EmojiCandidate> {
439
503
}
440
504
441
505
extension EmojiMatchQualityChecks on Subject <EmojiMatchQuality ?> {
442
- void get match => equals (EmojiMatchQuality .match);
506
+ void get exact => equals (EmojiMatchQuality .exact);
507
+ void get prefix => equals (EmojiMatchQuality .prefix);
508
+ void get other => equals (EmojiMatchQuality .other);
443
509
void get none => isNull ();
444
510
}
445
511
0 commit comments