@@ -307,9 +307,44 @@ void main() {
307307 check (view.results).single.which (
308308 isUnicodeResult (names: ['smile' ]));
309309 });
310+
311+ Future <Iterable <EmojiAutocompleteResult >> resultsOf (
312+ String query, {
313+ Map <String , String > realmEmoji = const {},
314+ Map <String , List <String >>? unicodeEmoji,
315+ }) async {
316+ final store = prepare (realmEmoji: realmEmoji, unicodeEmoji: unicodeEmoji);
317+ final view = EmojiAutocompleteView .init (store: store,
318+ query: EmojiAutocompleteQuery (query));
319+ bool done = false ;
320+ view.addListener (() { done = true ; });
321+ await Future (() {});
322+ check (done).isTrue ();
323+ return view.results;
324+ }
325+
326+ test ('results end-to-end' , () async {
327+ final unicodeEmoji = {
328+ '1f4d3' : ['notebook' ], '1f516' : ['bookmark' ], '1f4d6' : ['book' ]};
329+
330+ // Empty query -> base ordering.
331+ check (await resultsOf ('' , unicodeEmoji: unicodeEmoji)).deepEquals ([
332+ isUnicodeResult (names: ['notebook' ]),
333+ isUnicodeResult (names: ['bookmark' ]),
334+ isUnicodeResult (names: ['book' ]),
335+ isZulipResult (),
336+ ]);
337+
338+ // With query, exact match precedes prefix match precedes other.
339+ check (await resultsOf ('book' , unicodeEmoji: unicodeEmoji)).deepEquals ([
340+ isUnicodeResult (names: ['book' ]),
341+ isUnicodeResult (names: ['bookmark' ]),
342+ isUnicodeResult (names: ['notebook' ]),
343+ ]);
344+ });
310345 });
311346
312- group ('EmojiAutocompleteQuery.match ' , () {
347+ group ('EmojiAutocompleteQuery' , () {
313348 EmojiCandidate unicode (List <String > names, {String ? emojiCode}) {
314349 emojiCode ?? = '10ffff' ;
315350 return EmojiCandidate (emojiType: ReactionType .unicodeEmoji,
@@ -333,63 +368,71 @@ void main() {
333368 }
334369
335370 test ('one-word query matches anywhere in name' , () {
336- check (matchOfName ('' , 'smile' )).match ;
337- check (matchOfName ('s' , 'smile' )).match ;
338- check (matchOfName ('sm' , 'smile' )).match ;
339- check (matchOfName ('smile' , 'smile' )).match ;
340- check (matchOfName ('m' , 'smile' )).match ;
341- check (matchOfName ('mile' , 'smile' )).match ;
342- check (matchOfName ('e' , 'smile' )).match ;
371+ check (matchOfName ('' , 'smile' )).prefix ;
372+ check (matchOfName ('s' , 'smile' )).prefix ;
373+ check (matchOfName ('sm' , 'smile' )).prefix ;
374+ check (matchOfName ('smile' , 'smile' )).exact ;
375+ check (matchOfName ('m' , 'smile' )).other ;
376+ check (matchOfName ('mile' , 'smile' )).other ;
377+ check (matchOfName ('e' , 'smile' )).other ;
343378
344379 check (matchOfName ('smiley' , 'smile' )).none;
345380 check (matchOfName ('a' , 'smile' )).none;
346381
347- check (matchOfName ('o' , 'open_book' )).match ;
348- check (matchOfName ('open' , 'open_book' )).match ;
349- check (matchOfName ('pe' , 'open_book' )).match ;
350- check (matchOfName ('boo' , 'open_book' )).match ;
351- check (matchOfName ('ok' , 'open_book' )).match ;
382+ check (matchOfName ('o' , 'open_book' )).prefix ;
383+ check (matchOfName ('open' , 'open_book' )).prefix ;
384+ check (matchOfName ('pe' , 'open_book' )).other ;
385+ check (matchOfName ('boo' , 'open_book' )).other ;
386+ check (matchOfName ('ok' , 'open_book' )).other ;
352387 });
353388
354389 test ('multi-word query matches from start of a word' , () {
355- check (matchOfName ('open_' , 'open_book' )).match ;
356- check (matchOfName ('open_b' , 'open_book' )).match ;
357- check (matchOfName ('open_book' , 'open_book' )).match ;
390+ check (matchOfName ('open_' , 'open_book' )).prefix ;
391+ check (matchOfName ('open_b' , 'open_book' )).prefix ;
392+ check (matchOfName ('open_book' , 'open_book' )).exact ;
358393
359394 check (matchOfName ('pen_' , 'open_book' )).none;
360395 check (matchOfName ('n_b' , 'open_book' )).none;
361396
362- check (matchOfName ('blue_dia' , 'large_blue_diamond' )).match ;
397+ check (matchOfName ('blue_dia' , 'large_blue_diamond' )).other ;
363398 });
364399
365400 test ('spaces in query behave as underscores' , () {
366- check (matchOfName ('open ' , 'open_book' )).match ;
367- check (matchOfName ('open b' , 'open_book' )).match ;
368- check (matchOfName ('open book' , 'open_book' )).match ;
401+ check (matchOfName ('open ' , 'open_book' )).prefix ;
402+ check (matchOfName ('open b' , 'open_book' )).prefix ;
403+ check (matchOfName ('open book' , 'open_book' )).exact ;
369404
370405 check (matchOfName ('pen ' , 'open_book' )).none;
371406 check (matchOfName ('n b' , 'open_book' )).none;
372407
373- check (matchOfName ('blue dia' , 'large_blue_diamond' )).match ;
408+ check (matchOfName ('blue dia' , 'large_blue_diamond' )).other ;
374409 });
375410
376411 test ('query is lower-cased' , () {
377- check (matchOfName ('Smi' , 'smile' )).match ;
412+ check (matchOfName ('Smi' , 'smile' )).prefix ;
378413 });
379414
380415 test ('query matches aliases same way as primary name' , () {
381- check (matchOfNames ('a' , ['a' , 'b' ])).match ;
382- check (matchOfNames ('b' , ['a' , 'b' ])).match ;
416+ check (matchOfNames ('a' , ['a' , 'b' ])).exact ;
417+ check (matchOfNames ('b' , ['a' , 'b' ])).exact ;
383418 check (matchOfNames ('c' , ['a' , 'b' ])).none;
384419
385- check (matchOfNames ('pe' , ['x' , 'open_book' ])).match ;
386- check (matchOfNames ('ok' , ['x' , 'open_book' ])).match ;
420+ check (matchOfNames ('pe' , ['x' , 'open_book' ])).other ;
421+ check (matchOfNames ('ok' , ['x' , 'open_book' ])).other ;
387422
388- check (matchOfNames ('open_' , ['x' , 'open_book' ])).match ;
389- check (matchOfNames ('open b' , ['x' , 'open_book' ])).match ;
423+ check (matchOfNames ('open_' , ['x' , 'open_book' ])).prefix ;
424+ check (matchOfNames ('open b' , ['x' , 'open_book' ])).prefix ;
390425 check (matchOfNames ('pen_' , ['x' , 'open_book' ])).none;
391426
392- check (matchOfNames ('Smi' , ['x' , 'smile' ])).match;
427+ check (matchOfNames ('Smi' , ['x' , 'smile' ])).prefix;
428+ });
429+
430+ test ('best match among name and aliases prevails' , () {
431+ check (matchOfNames ('a' , ['ab' , 'a' , 'ba' , 'x' ])).exact;
432+ check (matchOfNames ('a' , ['ba' , 'ab' , 'x' ])).prefix;
433+ check (matchOfNames ('a' , ['ba' , 'ab' ])).prefix;
434+ check (matchOfNames ('a' , ['ba' , 'x' ])).other;
435+ check (matchOfNames ('a' , ['x' , 'y' , 'z' ])).none;
393436 });
394437
395438 test ('query matches literal Unicode value' , () {
@@ -403,13 +446,13 @@ void main() {
403446 check (matchOfLiteral ('1f642' , aka: '1f642' , '1f642' )).none;
404447
405448 // Matching the Unicode value the code describes does count…
406- check (matchOfLiteral ('🙂' , aka: '\u {1f642}' , '1f642' )).match ;
449+ check (matchOfLiteral ('🙂' , aka: '\u {1f642}' , '1f642' )).exact ;
407450 // … and failing to match it doesn't make a match.
408451 check (matchOfLiteral ('🙁' , aka: '\u {1f641}' , '1f642' )).none;
409452
410453 // Multi-code-point emoji work fine.
411454 check (matchOfLiteral ('🏳🌈' , aka: '\u {1f3f3}\u {200d}\u {1f308}' ,
412- '1f3f3-200d-1f308' )).match ;
455+ '1f3f3-200d-1f308' )).exact ;
413456 // Only exact matches count; no partial matches.
414457 check (matchOfLiteral ('🏳' , aka: '\u {1f3f3}' ,
415458 '1f3f3-200d-1f308' )).none;
@@ -430,11 +473,11 @@ void main() {
430473 resolvedStillUrl: eg.realmUrl.resolve ('/emoji/1-still.png' )));
431474 }
432475
433- check (matchOf ('eqeq' , realmCandidate ('eqeq' ))).match ;
434- check (matchOf ('open_' , realmCandidate ('open_book' ))).match ;
476+ check (matchOf ('eqeq' , realmCandidate ('eqeq' ))).exact ;
477+ check (matchOf ('open_' , realmCandidate ('open_book' ))).prefix ;
435478 check (matchOf ('n_b' , realmCandidate ('open_book' ))).none;
436- check (matchOf ('blue dia' , realmCandidate ('large_blue_diamond' ))).match ;
437- check (matchOf ('Smi' , realmCandidate ('smile' ))).match ;
479+ check (matchOf ('blue dia' , realmCandidate ('large_blue_diamond' ))).other ;
480+ check (matchOf ('Smi' , realmCandidate ('smile' ))).prefix ;
438481 });
439482
440483 test ('can match Zulip extra emoji' , () {
@@ -446,11 +489,32 @@ void main() {
446489 emojiType: ReactionType .zulipExtraEmoji,
447490 emojiCode: 'zulip' , emojiName: 'zulip' ));
448491
449- check (matchOf ('z' , zulipCandidate)).match ;
450- check (matchOf ('Zulip' , zulipCandidate)).match ;
451- check (matchOf ('p' , zulipCandidate)).match ;
492+ check (matchOf ('z' , zulipCandidate)).prefix ;
493+ check (matchOf ('Zulip' , zulipCandidate)).exact ;
494+ check (matchOf ('p' , zulipCandidate)).other ;
452495 check (matchOf ('x' , zulipCandidate)).none;
453496 });
497+
498+ int ? rankOf (String query, EmojiCandidate candidate) {
499+ return EmojiAutocompleteQuery (query).testCandidate (candidate)? .rank;
500+ }
501+
502+ void checkPrecedes (String query, EmojiCandidate a, EmojiCandidate b) {
503+ check (rankOf (query, a)! ).isLessThan (rankOf (query, b)! );
504+ }
505+
506+ test ('ranks exact before prefix before other match' , () {
507+ checkPrecedes ('o' , unicode (['o' ]), unicode (['onion' ]));
508+ checkPrecedes ('o' , unicode (['onion' ]), unicode (['book' ]));
509+ });
510+
511+ test ('full list of ranks' , () {
512+ check ([
513+ rankOf ('o' , unicode (['o' ])), // exact
514+ rankOf ('o' , unicode (['onion' ])), // prefix
515+ rankOf ('o' , unicode (['book' ])), // other
516+ ]).deepEquals ([0 , 1 , 2 ]);
517+ });
454518 });
455519}
456520
@@ -476,7 +540,9 @@ extension EmojiCandidateChecks on Subject<EmojiCandidate> {
476540}
477541
478542extension EmojiMatchQualityChecks on Subject <EmojiMatchQuality ?> {
479- void get match => equals (EmojiMatchQuality .match);
543+ void get exact => equals (EmojiMatchQuality .exact);
544+ void get prefix => equals (EmojiMatchQuality .prefix);
545+ void get other => equals (EmojiMatchQuality .other);
480546 void get none => isNull ();
481547}
482548
0 commit comments