snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
429
web/tests/typeahead.test.cjs
Normal file
429
web/tests/typeahead.test.cjs
Normal file
@@ -0,0 +1,429 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {zrequire} = require("./lib/namespace.cjs");
|
||||
const {run_test} = require("./lib/test.cjs");
|
||||
|
||||
const typeahead = zrequire("typeahead");
|
||||
|
||||
const unicode_emojis = [
|
||||
["1f43c", "panda_face"],
|
||||
["1f642", "slight_smile"],
|
||||
["1f604", "smile"],
|
||||
["1f368", "ice_cream"],
|
||||
["1f366", "soft_ice_cream"],
|
||||
["1f6a5", "horizontal_traffic_light"],
|
||||
["1f6a6", "traffic_light"],
|
||||
["1f537", "large_blue_diamond"],
|
||||
["1f539", "small_blue_diamond"],
|
||||
];
|
||||
|
||||
const emojis = [
|
||||
{emoji_name: "japanese_post_office", reaction_type: "realm_emoji", url: "TBD"},
|
||||
{emoji_name: "tada", reaction_type: "realm_emoji", random_field: "whatever"},
|
||||
...unicode_emojis.map(([emoji_code, emoji_name]) => ({
|
||||
emoji_name,
|
||||
emoji_code,
|
||||
reaction_type: "unicode_emoji",
|
||||
})),
|
||||
];
|
||||
|
||||
function emoji_matches(query) {
|
||||
const matcher = typeahead.get_emoji_matcher(query);
|
||||
return emojis.filter((emoji) => matcher(emoji));
|
||||
}
|
||||
|
||||
function assert_emoji_matches(query, expected) {
|
||||
const names = emoji_matches(query).map((emoji) => emoji.emoji_name);
|
||||
assert.deepEqual(names.toSorted(), expected);
|
||||
}
|
||||
|
||||
run_test("get_emoji_matcher: nonmatches", () => {
|
||||
assert_emoji_matches("notaemoji", []);
|
||||
assert_emoji_matches("da_", []);
|
||||
});
|
||||
|
||||
run_test("get_emoji_matcher: misc matches", () => {
|
||||
assert_emoji_matches("da", ["panda_face", "tada"]);
|
||||
assert_emoji_matches("smil", ["slight_smile", "smile"]);
|
||||
assert_emoji_matches("mile", ["slight_smile", "smile"]);
|
||||
assert_emoji_matches("japanese_post_", ["japanese_post_office"]);
|
||||
});
|
||||
|
||||
run_test("matches starting at non-first word, too", () => {
|
||||
assert_emoji_matches("ice_cream", ["ice_cream", "soft_ice_cream"]);
|
||||
assert_emoji_matches("blue_dia", ["large_blue_diamond", "small_blue_diamond"]);
|
||||
assert_emoji_matches("traffic_", ["horizontal_traffic_light", "traffic_light"]);
|
||||
});
|
||||
|
||||
run_test("matches literal unicode emoji", () => {
|
||||
assert_emoji_matches("🐼", ["panda_face"]);
|
||||
});
|
||||
|
||||
run_test("get_emoji_matcher: spaces equivalent to underscores", () => {
|
||||
function assert_equivalent(query) {
|
||||
assert.deepEqual(emoji_matches(query), emoji_matches(query.replace(" ", "_")));
|
||||
}
|
||||
assert_equivalent("da ");
|
||||
assert_equivalent("panda ");
|
||||
assert_equivalent("japanese post ");
|
||||
assert_equivalent("ice ");
|
||||
assert_equivalent("ice cream");
|
||||
assert_equivalent("blue dia");
|
||||
assert_equivalent("traffic ");
|
||||
assert_equivalent("traffic l");
|
||||
});
|
||||
|
||||
run_test("query_matches_string_in_order", () => {
|
||||
function assert_matches(query, source, split_char) {
|
||||
const should_remove_diacritics = !typeahead.contains_diacritics(query);
|
||||
assert.equal(
|
||||
typeahead.query_matches_string_in_order(
|
||||
query,
|
||||
source,
|
||||
split_char,
|
||||
should_remove_diacritics,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
function assert_no_match(query, source, split_char) {
|
||||
const should_remove_diacritics = !typeahead.contains_diacritics(query);
|
||||
assert.equal(
|
||||
typeahead.query_matches_string_in_order(
|
||||
query,
|
||||
source,
|
||||
split_char,
|
||||
should_remove_diacritics,
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// Query without diacritics should match source with diacritics.
|
||||
assert_matches("jose mar", "José María", " ");
|
||||
assert_matches("maria gon", "María González", " ");
|
||||
assert_matches("jose", "José María", " ");
|
||||
|
||||
// Query with diacritics does diacritic-sensitive matching
|
||||
assert_matches("josé", "José María", " ");
|
||||
assert_no_match("josé", "Jose Maria", " ");
|
||||
assert_matches("josé mar", "José María", " ");
|
||||
assert_no_match("josé mar", "Jose Maria", " ");
|
||||
});
|
||||
|
||||
run_test("triage", () => {
|
||||
const alice = {name: "alice"};
|
||||
const alicia = {name: "Alicia"};
|
||||
const joan = {name: "Joan"};
|
||||
const jo = {name: "Jo"};
|
||||
const steve = {name: "steve"};
|
||||
const stephanie = {name: "Stephanie"};
|
||||
|
||||
const names = [alice, alicia, joan, jo, steve, stephanie];
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("a", names, (r) => r.name),
|
||||
{
|
||||
matches: [alice, alicia],
|
||||
rest: [joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("A", names, (r) => r.name),
|
||||
{
|
||||
matches: [alicia, alice],
|
||||
rest: [joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("S", names, (r) => r.name),
|
||||
{
|
||||
matches: [stephanie, steve],
|
||||
rest: [alice, alicia, joan, jo],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("fred", names, (r) => r.name),
|
||||
{
|
||||
matches: [],
|
||||
rest: [alice, alicia, joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("Jo", names, (r) => r.name),
|
||||
{
|
||||
matches: [jo, joan],
|
||||
rest: [alice, alicia, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("jo", names, (r) => r.name),
|
||||
{
|
||||
matches: [jo, joan],
|
||||
rest: [alice, alicia, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage(" ", names, (r) => r.name),
|
||||
{
|
||||
matches: [],
|
||||
rest: [alice, alicia, joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage(";", names, (r) => r.name),
|
||||
{
|
||||
matches: [],
|
||||
rest: [alice, alicia, joan, jo, steve, stephanie],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
run_test("triage: prioritise word boundary matches to arbitrary substring matches", () => {
|
||||
const book = {name: "book"};
|
||||
const hyphen_ok = {name: "hyphen_ok"};
|
||||
const space_ok = {name: "space ok"};
|
||||
const no_space_ok = {name: "nospaceok"};
|
||||
const number_ok = {name: "number1ok"};
|
||||
const okay = {name: "okay"};
|
||||
const ok = {name: "ok"};
|
||||
|
||||
const emojis = [book, hyphen_ok, space_ok, no_space_ok, number_ok, okay, ok];
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("ok", emojis, (r) => r.name),
|
||||
{
|
||||
matches: [ok, okay, hyphen_ok, space_ok],
|
||||
rest: [book, no_space_ok, number_ok],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
run_test("triage: prioritize exact diacritic matches over diacritic-stripped matches", () => {
|
||||
const exact_a = {name: "a"};
|
||||
const exact_diacritic = {name: "ą"};
|
||||
const aaron = {name: "aaron"};
|
||||
const aa = {name: "Aa"};
|
||||
const adam_diacritic = {name: "Ądam"};
|
||||
const john_a = {name: "John a"};
|
||||
const zoe = {name: "Zoe"};
|
||||
|
||||
const users = [exact_a, exact_diacritic, aaron, aa, adam_diacritic, john_a, zoe];
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("a", users, (r) => r.name),
|
||||
{
|
||||
matches: [exact_a, aaron, aa, john_a],
|
||||
rest: [exact_diacritic, adam_diacritic, zoe],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("A", users, (r) => r.name),
|
||||
{
|
||||
matches: [exact_a, aa, aaron, john_a],
|
||||
rest: [exact_diacritic, adam_diacritic, zoe],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("ą", users, (r) => r.name),
|
||||
{
|
||||
matches: [exact_diacritic, adam_diacritic, exact_a, aaron, aa, john_a],
|
||||
rest: [zoe],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("Ą", users, (r) => r.name),
|
||||
{
|
||||
matches: [exact_diacritic, adam_diacritic, exact_a, aaron, aa, john_a],
|
||||
rest: [zoe],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
run_test("triage: ASCII query matches diacritic item via word boundary", () => {
|
||||
const luke_adam = {name: "Łuke adam"};
|
||||
const zoe = {name: "Zoe"};
|
||||
const users = [luke_adam, zoe];
|
||||
|
||||
assert.deepEqual(
|
||||
typeahead.triage("a", users, (r) => r.name),
|
||||
{
|
||||
matches: [luke_adam],
|
||||
rest: [zoe],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
run_test("triage_raw: diacritic-prefix and diacritic-stripped fallback", () => {
|
||||
const adam_diacritic = {names: ["Ądam"]};
|
||||
const joe_ascii = {names: ["Joe Maria"]};
|
||||
const alex_joe = {names: ["Alex joe"]};
|
||||
const zoe = {names: ["Zoe"]};
|
||||
const items = [adam_diacritic, joe_ascii, alex_joe, zoe];
|
||||
|
||||
const diacritic_result = typeahead.triage_raw("ą", items, (item) => item.names);
|
||||
assert.deepEqual(diacritic_result.begins_with_case_insensitive_diacritic_matches, [
|
||||
adam_diacritic,
|
||||
]);
|
||||
// The query has diacritics, so the diacritic-stripped fallback still lets an
|
||||
// ASCII candidate ("Alex joe") land in the case-insensitive bucket.
|
||||
assert.deepEqual(diacritic_result.begins_with_case_insensitive_matches, [alex_joe]);
|
||||
|
||||
const fallback_result = typeahead.triage_raw("joé", items, (item) => item.names);
|
||||
assert.deepEqual(fallback_result.begins_with_case_insensitive_matches, [joe_ascii]);
|
||||
assert.deepEqual(fallback_result.word_boundary_matches, [alex_joe]);
|
||||
});
|
||||
|
||||
function sort_emojis(emojis, query) {
|
||||
return typeahead.sort_emojis(emojis, query).map((emoji) => emoji.emoji_name);
|
||||
}
|
||||
|
||||
run_test("sort_emojis: th", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "mother_nature", is_realm_emoji: true},
|
||||
{emoji_name: "thermometer", is_realm_emoji: true},
|
||||
{emoji_name: "thumbs_down", is_realm_emoji: true},
|
||||
{emoji_name: "thumbs_up", is_realm_emoji: false, emoji_code: "1f44d"},
|
||||
];
|
||||
typeahead.set_frequently_used_emojis(typeahead.get_popular_emojis());
|
||||
assert.deepEqual(sort_emojis(emoji_list, "th"), [
|
||||
"thumbs_up",
|
||||
"thermometer",
|
||||
"thumbs_down",
|
||||
"mother_nature",
|
||||
]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: sm", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "smile", is_realm_emoji: true},
|
||||
{emoji_name: "slight_smile", is_realm_emoji: false, emoji_code: "1f642"},
|
||||
{emoji_name: "small_airplane", is_realm_emoji: true},
|
||||
];
|
||||
assert.deepEqual(sort_emojis(emoji_list, "sm"), ["slight_smile", "smile", "small_airplane"]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: SM", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "smile", is_realm_emoji: true},
|
||||
{emoji_name: "slight_smile", is_realm_emoji: false, emoji_code: "1f642"},
|
||||
{emoji_name: "small_airplane", is_realm_emoji: true},
|
||||
];
|
||||
assert.deepEqual(sort_emojis(emoji_list, "SM"), ["slight_smile", "smile", "small_airplane"]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: prefix before midphrase, with underscore (traffic_li)", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "horizontal_traffic_light", is_realm_emoji: true},
|
||||
{emoji_name: "traffic_light", is_realm_emoji: true},
|
||||
];
|
||||
assert.deepEqual(sort_emojis(emoji_list, "traffic_li"), [
|
||||
"traffic_light",
|
||||
"horizontal_traffic_light",
|
||||
]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: prefix before midphrase, with space (traffic li)", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "horizontal_traffic_light", is_realm_emoji: true},
|
||||
{emoji_name: "traffic_light", is_realm_emoji: true},
|
||||
];
|
||||
assert.deepEqual(sort_emojis(emoji_list, "traffic li"), [
|
||||
"traffic_light",
|
||||
"horizontal_traffic_light",
|
||||
]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: remove duplicates", () => {
|
||||
// notice the last 2 are aliases of the same emoji (same emoji code)
|
||||
const emoji_list = [
|
||||
{emoji_name: "laughter_tears", emoji_code: "1f602", is_realm_emoji: false},
|
||||
{emoji_name: "tear", emoji_code: "1f972", is_realm_emoji: false},
|
||||
{emoji_name: "smile_with_tear", emoji_code: "1f972", is_realm_emoji: false},
|
||||
];
|
||||
assert.deepEqual(typeahead.sort_emojis(emoji_list, "tear"), [emoji_list[1], emoji_list[0]]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: prioritise realm emojis", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "thank_you", emoji_code: "1f64f", is_realm_emoji: false},
|
||||
{
|
||||
emoji_name: "thank_you_custom",
|
||||
url: "something",
|
||||
is_realm_emoji: true,
|
||||
},
|
||||
];
|
||||
assert.deepEqual(typeahead.sort_emojis(emoji_list, "thank"), [emoji_list[1], emoji_list[0]]);
|
||||
});
|
||||
|
||||
run_test("sort_emojis: prioritise perfect matches", () => {
|
||||
const emoji_list = [
|
||||
{emoji_name: "thank_you", emoji_code: "1f64f", is_realm_emoji: false},
|
||||
{
|
||||
emoji_name: "thank_you_custom",
|
||||
url: "something",
|
||||
is_realm_emoji: true,
|
||||
},
|
||||
];
|
||||
assert.deepEqual(typeahead.sort_emojis(emoji_list, "thank you"), emoji_list);
|
||||
});
|
||||
|
||||
run_test("last_prefix_match", () => {
|
||||
let words = [
|
||||
"apple",
|
||||
"banana",
|
||||
"cantaloupe",
|
||||
"cherry",
|
||||
"kiwi",
|
||||
"melon",
|
||||
"pear",
|
||||
"plum",
|
||||
"raspberry",
|
||||
"watermelon",
|
||||
];
|
||||
let prefix = "p";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 7);
|
||||
|
||||
prefix = "ch";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 3);
|
||||
|
||||
prefix = "pom";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), null);
|
||||
|
||||
prefix = "aa";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), null);
|
||||
|
||||
prefix = "zu";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), null);
|
||||
|
||||
prefix = "";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 9);
|
||||
|
||||
words = ["one"];
|
||||
prefix = "one";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 0);
|
||||
|
||||
words = ["aa", "pr", "pra", "pre", "pri", "pro", "pru", "zz"];
|
||||
prefix = "pr";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 6);
|
||||
|
||||
words = ["same", "same", "same", "same", "same"];
|
||||
prefix = "same";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), 4);
|
||||
|
||||
words = [];
|
||||
prefix = "empty";
|
||||
assert.equal(typeahead.last_prefix_match(prefix, words), null);
|
||||
});
|
||||
Reference in New Issue
Block a user