Files
upstream-zulip/web/tests/search_suggestion.test.cjs
upstream-mirror 64898b2c79
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
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

1607 lines
53 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const {make_realm} = require("./lib/example_realm.cjs");
const {make_stream} = require("./lib/example_stream.cjs");
const {make_user} = require("./lib/example_user.cjs");
const {mock_esm, zrequire} = require("./lib/namespace.cjs");
const {run_test, noop} = require("./lib/test.cjs");
const {page_params} = require("./lib/zpage_params.cjs");
const narrow_state = mock_esm("../src/narrow_state");
const stream_topic_history_util = mock_esm("../src/stream_topic_history_util");
const direct_message_group_data = zrequire("direct_message_group_data");
const {Filter} = zrequire("filter");
const stream_data = zrequire("stream_data");
const stream_topic_history = zrequire("stream_topic_history");
const people = zrequire("people");
const search = zrequire("search_suggestion");
const {set_current_user, set_realm} = zrequire("state_data");
const current_user = {};
set_current_user(current_user);
const realm = make_realm();
set_realm(realm);
const me = make_user({
email: "myself@zulip.com",
user_id: 41,
full_name: "Me Myself",
});
const bob = make_user({
email: "bob@zulip.com",
user_id: 42,
full_name: "Bob Roberts",
});
const ted = make_user({
email: "ted@zulip.com",
delivery_email: "ted@zulip.com",
user_id: 101,
full_name: "Ted Smith",
});
const alice = make_user({
email: "alice@zulip.com",
user_id: 102,
full_name: "Alice Ignore",
});
const jeff = make_user({
email: "jeff@zulip.com",
user_id: 103,
full_name: "Jeff Zoolipson",
});
const example_avatar_url = "http://example.com/example.png";
let _stream_id = 0;
function new_stream_id() {
_stream_id += 1;
return _stream_id;
}
function init({override}) {
override(current_user, "is_admin", true);
// Add users to `valid_user_ids`.
const source = "server_events";
people.init();
people.add_active_user(bob, source);
people.add_active_user(me, source);
people.add_active_user(ted, source);
people.add_active_user(alice, source);
people.add_active_user(jeff, source);
people.initialize_current_user(me.user_id);
stream_topic_history.reset();
direct_message_group_data.clear_for_testing();
stream_data.clear_subscriptions();
}
function get_suggestions(query, pill_query = "") {
return search.get_suggestions(
Filter.parse(pill_query).map((suggestion) => Filter.convert_suggestion_to_term(suggestion)),
Filter.parse(query),
);
}
function test(label, f) {
run_test(label, (helpers) => {
init(helpers);
f(helpers);
});
}
test("person_multi_word_completion", () => {
// The help center documents searching person operators by full name,
// e.g. `sender:Elena García` and the multi-user `dm:Bo Lin, Elena
// García`. An unquoted multi-word name parses with a trailing `search:`
// term, which get_suggestions merges back into the person operand before
// resolving each name to a user id.
assert.deepEqual(get_suggestions("sender:Ted Smith"), [`sender:${ted.user_id}`]);
assert.deepEqual(get_suggestions("mentions:Ted Smith"), [`mentions:${ted.user_id}`]);
assert.deepEqual(get_suggestions("dm:Ted Smith"), [`dm:${ted.user_id}`]);
assert.deepEqual(get_suggestions("dm-including:Ted Smith"), [`dm-including:${ted.user_id}`]);
// dm and dm-including resolve a comma-separated list of names, with or
// without a space after the comma.
assert.deepEqual(get_suggestions("dm:Ted Smith, Alice Ignore"), [
`dm:${ted.user_id},${alice.user_id}`,
]);
assert.deepEqual(get_suggestions("dm:Ted Smith,Alice Ignore"), [
`dm:${ted.user_id},${alice.user_id}`,
]);
assert.deepEqual(get_suggestions("dm-including:Ted Smith, Alice Ignore"), [
`dm-including:${ted.user_id},${alice.user_id}`,
]);
assert.deepEqual(get_suggestions("dm-including:Ted Smith,Alice Ignore"), [
`dm-including:${ted.user_id},${alice.user_id}`,
]);
});
test("basic_get_suggestions", ({override}) => {
const query = "fred";
override(narrow_state, "stream_id", noop);
const suggestions = get_suggestions(query);
const expected = ["fred"];
assert.deepEqual(suggestions, expected);
});
test("basic_get_suggestions_for_spectator", () => {
page_params.is_spectator = true;
const web_public_id = new_stream_id();
const sub = make_stream({
name: "Web public",
stream_id: web_public_id,
is_web_public: true,
subscribed: false,
});
stream_data.add_sub_for_tests(sub);
let query = "";
let suggestions = get_suggestions(query);
assert.deepEqual(suggestions, [
"channels:",
"channel:",
"date:",
"is:resolved",
"-is:resolved",
"has:link",
"has:image",
"has:attachment",
"has:reaction",
]);
stream_data.delete_sub(sub.stream_id);
query = "channels:";
suggestions = get_suggestions(query);
assert.deepEqual(suggestions, ["channels:archived"]);
page_params.is_spectator = false;
});
test("get_suggestions deduplication", () => {
let query = "has:attachment";
let suggestions = get_suggestions(query, query);
let expected = ["has:attachment"];
assert.deepEqual(suggestions, expected);
query = "has:attachment has:attachment";
suggestions = get_suggestions(query);
expected = ["has:attachment"];
assert.deepEqual(suggestions, expected);
});
test("get_is_suggestions_for_spectator", () => {
page_params.is_spectator = true;
const query = "is:";
const suggestions = get_suggestions(query);
// The list of suggestions should only contain "is:resolved" for a spectator
assert.deepEqual(suggestions, ["is:resolved"]);
page_params.is_spectator = false;
});
test("dm_suggestions", ({override}) => {
let query = "is:dm";
let suggestions = get_suggestions(query);
// Empty query: compare_users_for_dms sorts alphabetically
// by full_name (no direct message history, and no name-length
// tiebreaker without a query).
let expected = [
"is:dm",
`dm:${alice.user_id}`, // Alice Ignore
`dm:${bob.user_id}`, // Bob Roberts
`dm:${jeff.user_id}`, // Jeff Zoolipson
`dm:${me.user_id}`, // Me Myself
`dm:${ted.user_id}`, // Ted Smith
];
assert.deepEqual(suggestions, expected);
query = "is:dm al";
suggestions = get_suggestions(query);
expected = [
"is:dm al",
"is:dm is:alerted",
`is:dm dm:${alice.user_id}`,
`is:dm sender:${alice.user_id}`,
`is:dm dm-including:${alice.user_id}`,
`is:dm mentions:${alice.user_id}`,
];
assert.deepEqual(suggestions, expected);
// "is:private" was renamed to "is:dm", so
// we suggest "is:dm" to anyone with "is:private"
// in their muscle memory.
query = "is:pr";
suggestions = get_suggestions(query);
expected = ["is:dm"];
assert.deepEqual(suggestions, expected);
query = "is:private";
suggestions = get_suggestions(query);
// Same search suggestions and order as for "is:dm".
expected = [
"is:dm",
`dm:${alice.user_id}`, // Alice Ignore
`dm:${bob.user_id}`, // Bob Roberts
`dm:${jeff.user_id}`, // Jeff Zoolipson
`dm:${me.user_id}`, // Me Myself
`dm:${ted.user_id}`, // Ted Smith
];
assert.deepEqual(suggestions, expected);
query = "dm:t";
suggestions = get_suggestions(query);
expected = [`dm:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "-dm:t";
suggestions = get_suggestions(query);
expected = [`is:dm -dm:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = `dm:${ted.user_id}`;
suggestions = get_suggestions(query);
expected = [`dm:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "sender:ted";
suggestions = get_suggestions(query);
expected = [`sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "sender:te";
suggestions = get_suggestions(query);
expected = [`sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "-sender:te";
suggestions = get_suggestions(query);
expected = [`-sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = `sender:${ted.user_id}`;
suggestions = get_suggestions(query);
expected = [`sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "is:unread from:ted";
suggestions = get_suggestions(query);
expected = [`is:unread sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
// Users can enter bizarre queries, and if they do, we want to
// be conservative with suggestions.
query = "is:dm near:3";
suggestions = get_suggestions(query);
expected = ["is:dm near:3"];
assert.deepEqual(suggestions, expected);
query = `dm:${ted.user_id} near:3`;
suggestions = get_suggestions(query);
expected = [`dm:${ted.user_id} near:3`];
assert.deepEqual(suggestions, expected);
// Make sure suggestions still work if preceding tokens
query = `is:alerted sender:${ted.user_id}`;
suggestions = get_suggestions(query);
expected = [`is:alerted sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
query = "is:starred has:link is:dm al";
suggestions = get_suggestions(query);
expected = [
"is:starred has:link is:dm al",
"is:starred has:link is:dm is:alerted",
`is:starred has:link is:dm dm:${alice.user_id}`,
`is:starred has:link is:dm sender:${alice.user_id}`,
`is:starred has:link is:dm dm-including:${alice.user_id}`,
`is:starred has:link is:dm mentions:${alice.user_id}`,
];
assert.deepEqual(suggestions, expected);
query = `from:${ted.user_id}`;
suggestions = get_suggestions(query);
expected = [`sender:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
// "pm-with" operator returns search result
// and "dm" operator as a suggestions
override(narrow_state, "stream_id", () => undefined);
query = "pm-with";
suggestions = get_suggestions(query);
expected = ["pm-with", "dm:"];
assert.deepEqual(suggestions, expected);
query = "pm-with:t";
suggestions = get_suggestions(query);
expected = [`dm:${ted.user_id}`];
assert.deepEqual(suggestions, expected);
});
test("dm_suggestions_for_exact_full_name", () => {
// When the typed text exactly matches a unique full name, it is
// resolved to that user's id internally. We must still suggest other
// users whose names match the typed text as a prefix, rather than
// collapsing to just the exactly-named user.
const john = make_user({
email: "john@zulip.com",
user_id: 200,
full_name: "John",
});
const john_doe = make_user({
email: "john-doe@zulip.com",
user_id: 201,
full_name: "John Doe",
});
people.add_active_user(john, "server_events");
people.add_active_user(john_doe, "server_events");
// A partial query matches both users by name prefix.
let suggestions = get_suggestions("dm:joh");
assert.deepEqual(suggestions, [`dm:${john.user_id}`, `dm:${john_doe.user_id}`]);
// Completing the unique full name "John" must still suggest "John Doe",
// not collapse to just "John".
suggestions = get_suggestions("dm:John");
assert.deepEqual(suggestions, [`dm:${john.user_id}`, `dm:${john_doe.user_id}`]);
// A multi-word continuation after the resolved name still matches by
// name, narrowing to "John Doe".
suggestions = get_suggestions("dm:John Do");
assert.deepEqual(suggestions, [`dm:${john_doe.user_id}`]);
});
test("group_suggestions", () => {
// If there's an existing completed user pill right before
// the input string, we suggest a user group as one of the
// suggestions.
let pill_query = `dm:${bob.user_id}`;
let query = "alice";
let suggestions = get_suggestions(query, pill_query);
let expected = [
`dm:${bob.user_id} alice`,
`dm:${bob.user_id},${alice.user_id}`,
`dm:${bob.user_id} sender:${alice.user_id}`,
`dm:${bob.user_id} dm-including:${alice.user_id}`,
`dm:${bob.user_id} mentions:${alice.user_id}`,
];
assert.deepEqual(suggestions, expected);
// Do not suggest "myself@zulip.com" (the name of the current user) for dms
pill_query = `dm:${ted.user_id}`;
query = "my";
suggestions = get_suggestions(query, pill_query);
expected = [
`dm:${ted.user_id} my`,
`dm:${ted.user_id} sender:${me.user_id}`,
`dm:${ted.user_id} dm-including:${me.user_id}`,
`dm:${ted.user_id} mentions:${me.user_id}`,
];
assert.deepEqual(suggestions, expected);
// "is:dm" should be properly prepended to each suggestion
// if the "dm" operator is negated.
query = "-dm:bob@zulip.co";
suggestions = get_suggestions(query);
expected = [`is:dm -dm:${bob.user_id}`];
assert.deepEqual(suggestions, expected);
// If user types "pm-with" operator, show suggestions for
// group direct messages with the "dm" operator.
pill_query = `pm-with:${bob.user_id}`;
query = "alice";
suggestions = get_suggestions(query, pill_query);
expected = [
`dm:${bob.user_id} alice`,
`dm:${bob.user_id},${alice.user_id}`,
`dm:${bob.user_id} sender:${alice.user_id}`,
`dm:${bob.user_id} dm-including:${alice.user_id}`,
`dm:${bob.user_id} mentions:${alice.user_id}`,
];
assert.deepEqual(suggestions, expected);
// Test multiple terms
pill_query = `is:starred has:link dm:${bob.user_id}`;
query = "Smit";
suggestions = get_suggestions(query, pill_query);
expected = [
`is:starred has:link dm:${bob.user_id} Smit`,
`is:starred has:link dm:${bob.user_id},${ted.user_id}`,
`is:starred has:link dm:${bob.user_id} sender:${ted.user_id}`,
`is:starred has:link dm:${bob.user_id} dm-including:${ted.user_id}`,
`is:starred has:link dm:${bob.user_id} mentions:${ted.user_id}`,
];
assert.deepEqual(suggestions, expected);
// Doesn't show dms because it's invalid in combination
// with a channel. (Random channel id.)
stream_data.add_sub_for_tests(
make_stream({
stream_id: 66,
name: "misc",
subscribed: true,
}),
);
query = `channel:66 has:link dm:${bob.user_id},Smit`;
suggestions = get_suggestions(query);
expected = [];
assert.deepEqual(suggestions, expected);
// Invalid emails don't give suggestions
query = "dm:invalid@zulip.com,Smit";
suggestions = get_suggestions(query);
expected = [];
assert.deepEqual(suggestions, expected);
});
test("empty_query_suggestions", () => {
const query = "";
const devel_id = new_stream_id();
const office_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: devel_id,
name: "devel",
subscribed: true,
is_web_public: true,
}),
);
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_id,
name: "office",
subscribed: true,
}),
);
const suggestions = get_suggestions(query);
const expected = [
"channels:",
"channel:",
"date:",
"is:dm",
"is:starred",
"is:mentioned",
"is:followed",
"is:alerted",
"is:unread",
"is:muted",
"is:resolved",
"-is:resolved",
`sender:${me.user_id}`,
`channel:${devel_id}`,
`channel:${office_id}`,
"has:link",
"has:image",
"has:attachment",
"has:reaction",
];
assert.deepEqual(suggestions, expected);
});
test("has_suggestions", ({override}) => {
// Checks that category wise suggestions are displayed instead of a single
// default suggestion when suggesting `has` operator.
let query = "h";
stream_data.add_sub_for_tests(
make_stream({
stream_id: 44,
name: "devel",
subscribed: true,
}),
);
stream_data.add_sub_for_tests(
make_stream({
stream_id: 77,
name: "office",
subscribed: true,
}),
);
override(narrow_state, "stream_id", noop);
let suggestions = get_suggestions(query);
let expected = ["h", "has:link", "has:image", "has:attachment", "has:reaction"];
assert.deepEqual(suggestions, expected);
query = "-h";
suggestions = get_suggestions(query);
expected = ["-h", "-has:link", "-has:image", "-has:attachment", "-has:reaction"];
assert.deepEqual(suggestions, expected);
// operand suggestions follow.
query = "has:";
suggestions = get_suggestions(query);
expected = ["has:link", "has:image", "has:attachment", "has:reaction"];
assert.deepEqual(suggestions, expected);
query = "has:im";
suggestions = get_suggestions(query);
expected = ["has:image"];
assert.deepEqual(suggestions, expected);
query = "-has:im";
suggestions = get_suggestions(query);
expected = ["-has:image"];
assert.deepEqual(suggestions, expected);
query = "att";
suggestions = get_suggestions(query);
expected = ["att", "has:attachment"];
assert.deepEqual(suggestions, expected);
stream_data.add_sub_for_tests(
make_stream({
stream_id: 66,
name: "misc",
subscribed: true,
}),
);
query = "channel:66 is:alerted has:lin";
suggestions = get_suggestions(query);
expected = ["channel:66 is:alerted has:link"];
assert.deepEqual(suggestions, expected);
});
test("check_is_suggestions", ({override}) => {
override(narrow_state, "stream_id", noop);
let query = "i";
let suggestions = get_suggestions(query);
let expected = [
"i",
"is:dm",
"is:starred",
"is:mentioned",
"is:followed",
"is:alerted",
"is:unread",
"is:muted",
"is:resolved",
`dm:${alice.user_id}`,
`sender:${alice.user_id}`,
`dm-including:${alice.user_id}`,
`mentions:${alice.user_id}`,
"has:image",
];
assert.deepEqual(suggestions, expected);
query = "-i";
suggestions = get_suggestions(query);
expected = [
"-i",
"-is:dm",
"-is:starred",
"-is:mentioned",
"-is:followed",
"-is:alerted",
"-is:unread",
"-is:muted",
"-is:resolved",
];
assert.deepEqual(suggestions, expected);
// operand suggestions follow.
query = "is:";
suggestions = get_suggestions(query);
expected = [
"is:dm",
"is:starred",
"is:mentioned",
"is:followed",
"is:alerted",
"is:unread",
"is:muted",
"is:resolved",
];
assert.deepEqual(suggestions, expected);
query = "is:st";
suggestions = get_suggestions(query);
expected = ["is:starred"];
assert.deepEqual(suggestions, expected);
query = "-is:st";
suggestions = get_suggestions(query);
expected = ["-is:starred"];
assert.deepEqual(suggestions, expected);
// Still returns suggestions for "streams:public",
// but shows html description used for "channels:public"
query = "st";
suggestions = get_suggestions(query);
expected = ["st", "channels:", "channel:", "is:starred"];
assert.deepEqual(suggestions, expected);
stream_data.add_sub_for_tests(
make_stream({
stream_id: 66,
name: "misc",
subscribed: true,
}),
);
query = "channel:66 has:link is:sta";
suggestions = get_suggestions(query);
expected = ["channel:66 has:link is:starred"];
assert.deepEqual(suggestions, expected);
});
test("sent_by_me_suggestions", ({override}) => {
override(narrow_state, "stream_id", noop);
override(stream_topic_history_util, "get_server_history", noop);
let query = "";
let suggestions = get_suggestions(query);
assert.ok(suggestions.includes(`sender:${me.user_id}`));
query = "sender";
suggestions = get_suggestions(query);
let expected = ["sender", "sender:", `sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
query = "-sender";
suggestions = get_suggestions(query);
expected = ["-sender", "-sender:", `-sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
query = "from";
suggestions = get_suggestions(query);
expected = ["from", "sender:", `sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
query = "-from";
suggestions = get_suggestions(query);
expected = ["-from", "-sender:", `-sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
query = `sender:${bob.user_id}`;
suggestions = get_suggestions(query);
expected = [`sender:${bob.user_id}`];
assert.deepEqual(suggestions, expected);
query = `from:${bob.user_id}`;
suggestions = get_suggestions(query);
expected = [`sender:${bob.user_id}`];
assert.deepEqual(suggestions, expected);
query = "sent";
suggestions = get_suggestions(query);
expected = ["sent", `sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
query = "-sent";
suggestions = get_suggestions(query);
expected = ["-sent", `-sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
const denmark_id = new_stream_id();
const sub = make_stream({
name: "Denmark",
stream_id: denmark_id,
});
stream_data.add_sub_for_tests(sub);
// `sent` is an exact keyword shorthand for the sent-by-me
// suggestion (`get_sent_by_me_suggestions` → `sender:me`), which
// tier 2 surfaces above the literal-input row.
query = `channel:${denmark_id} topic:Denmark1 sent`;
suggestions = get_suggestions(query);
expected = [
`channel:${denmark_id} topic:Denmark1 sender:${me.user_id}`,
`channel:${denmark_id} topic:Denmark1 sent`,
];
assert.deepEqual(suggestions, expected);
query = "is:starred sender:m";
suggestions = get_suggestions(query);
expected = [`is:starred sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
});
test("topic_suggestions", ({override, override_rewire}) => {
let suggestions;
let expected;
override(stream_topic_history_util, "get_server_history", noop);
const office_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_id,
name: "office",
subscribed: true,
}),
);
override(narrow_state, "stream_id", () => office_id);
const devel_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: devel_id,
name: "devel",
subscribed: true,
}),
);
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_id,
name: "office",
subscribed: true,
}),
);
suggestions = get_suggestions("te");
expected = [
"te",
`dm:${ted.user_id}`,
`sender:${ted.user_id}`,
`dm-including:${ted.user_id}`,
`mentions:${ted.user_id}`,
];
assert.deepEqual(suggestions, expected);
stream_topic_history.add_message({
stream_id: devel_id,
topic_name: "REXX",
});
for (const topic_name of ["team", "ignore", "✔ ice cream", "✔ team work", "test"]) {
stream_topic_history.add_message({
stream_id: office_id,
topic_name,
});
}
suggestions = get_suggestions("te");
expected = [
"te",
`dm:${ted.user_id}`,
`sender:${ted.user_id}`,
`dm-including:${ted.user_id}`,
`mentions:${ted.user_id}`,
`channel:${office_id} topic:team`,
`channel:${office_id} topic:✔+team+work`,
`channel:${office_id} topic:test`,
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`topic:staplers channel:${office_id}`);
expected = [`topic:staplers channel:${office_id}`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`channel:${devel_id} topic:`);
expected = [`channel:${devel_id} topic:`, `channel:${devel_id} topic:REXX`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`channel:${devel_id} -topic:`);
expected = [`channel:${devel_id} -topic:`, `channel:${devel_id} -topic:REXX`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("-topic:te");
expected = [
"-topic:te",
`channel:${office_id} -topic:team`,
`channel:${office_id} -topic:✔+team+work`,
`channel:${office_id} -topic:test`,
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("topic:", `-channel:${office_id}`);
expected = [
`-channel:${office_id} topic:`,
`-channel:${office_id} channel:${devel_id} topic:REXX`,
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("topic:", `-channel:${office_id} -channel:${devel_id}`);
expected = [`-channel:${office_id} -channel:${devel_id} topic:`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`is:alerted channel:${devel_id} is:starred topic:`);
expected = [
`is:alerted channel:${devel_id} is:starred topic:`,
`is:alerted channel:${devel_id} is:starred topic:REXX`,
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`is:dm channel:${devel_id} topic:`);
expected = [`is:dm channel:${devel_id} topic:`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions(`topic:REXX channel:${devel_id} topic:`);
expected = [`topic:REXX channel:${devel_id} topic:`];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("topic:");
expected = [
"topic:",
"channel:5 topic:✔+ice+cream",
"channel:5 topic:ignore",
`channel:5 topic:team`,
"channel:5 topic:✔+team+work",
`channel:5 topic:test`,
"channel:6 topic:REXX",
];
assert.deepEqual(suggestions, expected);
override(narrow_state, "stream_id", () => "");
for (const topic_name of ["a", "b", "c", "trap", "talks", "tower"]) {
stream_topic_history.add_message({
stream_id: devel_id,
topic_name,
});
}
override_rewire(stream_data, "set_max_channel_width_css_variable", noop);
stream_data.subscribe_myself(stream_data.get_sub("devel"));
stream_data.subscribe_myself(stream_data.get_sub("office"));
suggestions = get_suggestions("topic:");
expected = [
"topic:",
"channel:6 topic:a",
"channel:6 topic:b",
"channel:6 topic:c",
"channel:5 topic:✔+ice+cream",
"channel:5 topic:ignore",
"channel:6 topic:REXX",
"channel:5 topic:team",
"channel:5 topic:✔+team+work",
"channel:5 topic:test",
"channel:6 topic:trap",
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("topic:t");
expected = [
"topic:t",
"channel:5 topic:team",
"channel:5 topic:✔+team+work",
"channel:5 topic:test",
"channel:6 topic:trap",
"channel:6 topic:talks",
"channel:6 topic:tower",
];
assert.deepEqual(suggestions, expected);
// Prioritize topics from currently narrowed channel
override(narrow_state, "stream_id", () => devel_id);
suggestions = get_suggestions("topic:t");
expected = [
"topic:t",
"channel:6 topic:trap",
"channel:6 topic:talks",
"channel:6 topic:tower",
"channel:5 topic:team",
"channel:5 topic:✔+team+work",
"channel:5 topic:test",
];
assert.deepEqual(suggestions, expected);
override(narrow_state, "stream_id", () => office_id);
suggestions = get_suggestions("topic:t");
expected = [
"topic:t",
"channel:5 topic:team",
"channel:5 topic:✔+team+work",
"channel:5 topic:test",
"channel:6 topic:trap",
"channel:6 topic:talks",
"channel:6 topic:tower",
];
assert.deepEqual(suggestions, expected);
// This is the case where the last term's
// operand is not a stringified channel id.
// We avoid suggesting topics in this case.
suggestions = get_suggestions("channel:foobar");
expected = [];
assert.deepEqual(suggestions, expected);
// We shouldn't see topic suggestions for the case
// where the last term is a channel with an operand
// that couldn't be converted to a string id by the
// Filter.parse step.
suggestions = get_suggestions("hello channel:foobar");
expected = [];
assert.deepEqual(suggestions, expected);
// We shouldn't see topic suggestions for an
// empty operand with channel as the operator for
// the last term.
suggestions = get_suggestions("hello channel:");
expected = ["hello channel:5", "hello channel:6"];
assert.deepEqual(suggestions, expected);
});
test("topic_suggestions (limits)", () => {
let candidate_topic_entries = [];
function wrap(topics) {
return topics.map((t) => ({channel_id: "1", topic: t}));
}
function assert_result(guess, expected_topic_strings) {
assert.deepEqual(
search.get_topic_suggestions_from_candidates({
candidate_topic_entries,
guess,
}),
wrap(expected_topic_strings),
);
}
assert_result("", []);
assert_result("zzz", []);
candidate_topic_entries = wrap(["a", "b", "c"]);
assert_result("", ["a", "b", "c"]);
assert_result("b", ["b"]);
assert_result("z", []);
candidate_topic_entries = wrap([
"a1",
"a2",
"b1",
"b2",
"a3",
"a4",
"a5",
"c1",
"a6",
"a7",
"a8",
"c2",
"a9",
"a10",
"a11",
"a12",
]);
// We max out at 10 topics, so as not to overwhelm the user.
assert_result("", ["a1", "a2", "b1", "b2", "a3", "a4", "a5", "c1", "a6", "a7"]);
assert_result("a", ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"]);
assert_result("b", ["b1", "b2"]);
assert_result("z", []);
});
test("whitespace_glitch", ({override}) => {
const office_stream_id = new_stream_id();
const query = "channel:office "; // note trailing space
override(stream_topic_history_util, "get_server_history", noop);
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_stream_id,
name: "office",
subscribed: true,
}),
);
const suggestions = get_suggestions(query);
const expected = [`channel:${office_stream_id}`];
assert.deepEqual(suggestions, expected);
});
test("channel_completion", ({override}) => {
const office_stream_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_stream_id,
name: "office",
subscribed: true,
}),
);
const dev_help_stream_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: dev_help_stream_id,
name: "dev help",
subscribed: true,
}),
);
override(narrow_state, "stream_id", noop);
let query = "channel:of";
let suggestions = get_suggestions(query);
let expected = [`channel:${office_stream_id}`];
assert.deepEqual(suggestions, expected);
query = "-channel:of";
suggestions = get_suggestions(query);
expected = [`-channel:${office_stream_id}`];
assert.deepEqual(suggestions, expected);
query = "hel";
suggestions = get_suggestions(query);
expected = ["hel", `channel:${dev_help_stream_id}`];
assert.deepEqual(suggestions, expected);
});
test("channel_exact_name_surfaces_siblings", ({override}) => {
override(stream_topic_history_util, "get_server_history", noop);
const core_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: core_id, name: "core", subscribed: true}),
);
const core_team_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: core_team_id, name: "core team", subscribed: true}),
);
// Typing the exact name "core" resolves to its stream_id via
// `Filter.parse`, but we still phrase-match the name so the
// sibling channel "core team" surfaces as an alternative
// interpretation, just as it would for the still-incomplete
// "channel:cor".
let query = "channel:core";
let suggestions = get_suggestions(query);
let expected = [`channel:${core_id}`, `channel:${core_team_id}`];
assert.deepEqual(suggestions, expected);
// The still-incomplete name behaves identically, confirming the
// exact-match case isn't a special-cased regression.
query = "channel:cor";
suggestions = get_suggestions(query);
expected = [`channel:${core_id}`, `channel:${core_team_id}`];
assert.deepEqual(suggestions, expected);
// A numeric operand resolves to that stream_id's channel, but we
// also match it as literally typed, so `channel:7` surfaces both
// #Venice (id 7) and a channel whose name starts with "7".
stream_data.add_sub_for_tests(make_stream({stream_id: 7, name: "Venice", subscribed: true}));
const seventh_floor_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: seventh_floor_id, name: "7th floor", subscribed: true}),
);
query = "channel:7";
suggestions = get_suggestions(query);
expected = ["channel:7", `channel:${seventh_floor_id}`];
assert.deepEqual(suggestions, expected);
// "Venice" (id 7) has a genuine sibling "Venice beach". Typing the
// exact name resolves the operand to the id "7", so we match both
// the name "Venice" and the literal "7". The genuine sibling
// "Venice beach" (a name match) must rank above "7th floor" (only a
// coincidental match on the id-7 literal).
const venice_beach_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: venice_beach_id, name: "Venice beach", subscribed: true}),
);
query = "channel:Venice";
suggestions = get_suggestions(query);
expected = ["channel:7", `channel:${venice_beach_id}`, `channel:${seventh_floor_id}`];
assert.deepEqual(suggestions, expected);
});
test("channel_multi_word_completion", ({override}) => {
override(narrow_state, "stream_id", noop);
override(stream_topic_history_util, "get_server_history", noop);
const automated_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: automated_id, name: "automated", subscribed: true}),
);
const automated_testing_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: automated_testing_id,
name: "automated testing",
subscribed: true,
}),
);
const q1_automated_testing_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: q1_automated_testing_id,
name: "Q1 automated testing",
subscribed: true,
}),
);
// Strong multi-word match ("automated testing" starts with the
// typed query) ranks above the literal-input row, weak match ("Q1
// automated testing" matches but doesn't start with the query)
// ranks below.
let query = "channel:automated testing";
let suggestions = get_suggestions(query);
let expected = [
`channel:${automated_testing_id}`,
`channel:${automated_id} testing`,
`channel:${q1_automated_testing_id}`,
];
assert.deepEqual(suggestions, expected);
// Trailing fragment is a prefix of `has:*` with no multi-word
// match, so tier 2 surfaces those completions above the
// literal-input row. Only fires because the single-word channel
// `automated` resolves.
query = "channel:automated h";
suggestions = get_suggestions(query);
expected = [
`channel:${automated_id} has:link`,
`channel:${automated_id} has:image`,
`channel:${automated_id} has:attachment`,
`channel:${automated_id} has:reaction`,
`channel:${automated_id} h`,
];
assert.deepEqual(suggestions, expected);
// Single-word channel doesn't resolve. Strong multi-word match
// floats to the top. Tier 3 drops the un-resolvable channel and
// shows just the trailing fragment. The post-tier filterer loop
// also offers channels matching the trailing fragment alone.
query = "channel:Q1 automated";
suggestions = get_suggestions(query);
expected = [
`channel:${q1_automated_testing_id}`,
"automated",
`channel:${automated_id}`,
`channel:${automated_testing_id}`,
];
assert.deepEqual(suggestions, expected);
// No multi-word match and the typed channel doesn't resolve: tier
// 3 row degrades to just the trailing fragment.
query = "channel:nonexistent foo";
suggestions = get_suggestions(query);
expected = ["foo"];
assert.deepEqual(suggestions, expected);
// Resolved single-word channel with no multi-word match: tier 3
// shows the resolved channel + free-text search.
query = "channel:automated nonexistent";
suggestions = get_suggestions(query);
expected = [`channel:${automated_id} nonexistent`];
assert.deepEqual(suggestions, expected);
// When the channel resolves, the post-tier filterer loop runs with
// the resolved-channel context, surfacing e.g. topic suggestions.
stream_topic_history.add_message({stream_id: automated_id, topic_name: "release prep"});
query = "channel:automated release";
suggestions = get_suggestions(query);
expected = [`channel:${automated_id} release`, `channel:${automated_id} topic:release+prep`];
assert.deepEqual(suggestions, expected);
// `se` is an operator-prefix fragment: tier 2 surfaces both the
// `sender:` operator completion and the sent-by-me completion.
query = "channel:automated se";
suggestions = get_suggestions(query);
expected = [
`channel:${automated_id} sender:`,
`channel:${automated_id} sender:${me.user_id}`,
`channel:${automated_id} se`,
];
assert.deepEqual(suggestions, expected);
// With the same channel already pilled, the tier-3 row shows the
// channel only once, and multi-word channel matches (which would
// add a second channel term) are suppressed as invalid.
suggestions = get_suggestions("channel:automated testing", "channel:automated");
expected = [`channel:${automated_id} testing`];
assert.deepEqual(suggestions, expected);
// An `is:dm` pill is incompatible with channel terms, so no
// multi-word channel matches are offered; only the literal input
// row remains.
suggestions = get_suggestions("channel:automated testing", "is:dm");
expected = [`is:dm channel:${automated_id} testing`];
assert.deepEqual(suggestions, expected);
// When the trailing fragment is both a strong multi-word match and
// an operator prefix ("h" prefixes `has:*`), the multi-word channel
// match ("deploy hooks") ranks above the operator completions: a
// two-word channel name is the likelier intent than starting a new
// operator.
const deploy_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: deploy_id, name: "deploy", subscribed: true}),
);
const deploy_hooks_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: deploy_hooks_id, name: "deploy hooks", subscribed: true}),
);
suggestions = get_suggestions("channel:deploy h");
expected = [
`channel:${deploy_hooks_id}`,
`channel:${deploy_id} has:link`,
`channel:${deploy_id} has:image`,
`channel:${deploy_id} has:attachment`,
`channel:${deploy_id} has:reaction`,
`channel:${deploy_id} h`,
];
assert.deepEqual(suggestions, expected);
});
test("channel_multi_word_renamed_completion", ({override, override_rewire}) => {
override(narrow_state, "stream_id", noop);
override_rewire(stream_data, "set_max_channel_width_css_variable", noop);
const pipelines_id = new_stream_id();
const pipelines_sub = make_stream({
stream_id: pipelines_id,
name: "pipeline",
subscribed: true,
});
stream_data.add_sub_for_tests(pipelines_sub);
stream_data.rename_sub(pipelines_sub, "pipelines");
// A channel's pre-rename name is not resolved (matching
// `Filter.parse`'s name resolution), so it reads as free text.
const suggestions = get_suggestions("channel:pipeline x");
const expected = ["x"];
assert.deepEqual(suggestions, expected);
});
test("channel_multi_word_digit_prefix_completion", ({override}) => {
override(narrow_state, "stream_id", noop);
// "7th floor" starts with digits. `get_sub_by_id_string` accepts
// only an exact canonical id, so "7th" isn't read as the unrelated
// channel with id 7 — it's matched by name to "7th floor"
// (channel:8). "7th" also isn't a valid channel id, so there's no
// `channel:7th` literal row.
stream_data.add_sub_for_tests(make_stream({stream_id: 7, name: "general", subscribed: true}));
stream_data.add_sub_for_tests(make_stream({stream_id: 8, name: "7th floor", subscribed: true}));
let suggestions = get_suggestions("channel:7th");
let expected = ["channel:8"];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("channel:7th fl");
expected = ["channel:8", "fl"];
assert.deepEqual(suggestions, expected);
});
test("channel_multi_word_name_starting_with_id", ({override}) => {
override(stream_topic_history_util, "get_server_history", noop);
// A channel named "15 test" whose leading word ("15") is also
// another channel's stream_id. Typing `channel:15 test` resolves
// "15" to the id-15 channel for the merged query, but we also match
// the query as literally typed, so "15 test" still surfaces by name.
stream_data.add_sub_for_tests(make_stream({stream_id: 15, name: "lobby", subscribed: true}));
const fifteen_test_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: fifteen_test_id, name: "15 test", subscribed: true}),
);
const suggestions = get_suggestions("channel:15 test");
const expected = [`channel:${fifteen_test_id}`, "channel:15 test"];
assert.deepEqual(suggestions, expected);
});
test("topic_multi_word_completion", ({override}) => {
override(narrow_state, "stream_id", noop);
const releases_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: releases_id, name: "releases", subscribed: true}),
);
stream_topic_history.add_message({stream_id: releases_id, topic_name: "release notes"});
stream_topic_history.add_message({
stream_id: releases_id,
message_id: 1,
topic_name: "release hotfix",
});
// Strong multi-word topic match (cross-channel, since no channel
// pill) ranks above the pinned default-row.
let query = "topic:release notes";
let suggestions = get_suggestions(query);
let expected = [`channel:${releases_id} topic:release+notes`, "topic:release notes"];
assert.deepEqual(suggestions, expected);
// When the trailing fragment is both a strong multi-word match and
// an operator prefix ("h" prefixes `has:*`), the multi-word topic
// match ("release hotfix") ranks above the operator completions,
// which precede the literal-input row.
query = "topic:release h";
suggestions = get_suggestions(query);
expected = [
`channel:${releases_id} topic:release+hotfix`,
"topic:release has:link",
"topic:release has:image",
"topic:release has:attachment",
"topic:release has:reaction",
"topic:release h",
];
assert.deepEqual(suggestions, expected);
// An `is:dm` pill is incompatible with topic terms, so no
// multi-word topic matches are offered; only the literal input
// row remains.
query = "topic:release notes";
suggestions = get_suggestions(query, "is:dm");
expected = ["is:dm topic:release notes"];
assert.deepEqual(suggestions, expected);
// A resolved topic ranks as a strong match: classification ignores
// the resolved-topic prefix, like the sort key does.
stream_topic_history.add_message({
stream_id: releases_id,
message_id: 2,
topic_name: "✔ release docs",
});
query = "topic:release docs";
suggestions = get_suggestions(query);
expected = [`channel:${releases_id} topic:✔+release+docs`, "topic:release docs"];
assert.deepEqual(suggestions, expected);
});
test("topic_multi_word_match_budget", ({override}) => {
override(stream_topic_history_util, "get_server_history", noop);
const narrow_stream_id = new_stream_id();
override(narrow_state, "stream_id", () => narrow_stream_id);
stream_data.add_sub_for_tests(
make_stream({stream_id: narrow_stream_id, name: "core team", subscribed: true}),
);
const releases_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({stream_id: releases_id, name: "releases", subscribed: true}),
);
// Enough weak matches in the narrowed channel to exhaust a
// 10-topic match budget on their own.
for (let i = 1; i <= 10; i += 1) {
stream_topic_history.add_message({
stream_id: narrow_stream_id,
message_id: i,
topic_name: `old release notes ${i}`,
});
}
stream_topic_history.add_message({
stream_id: releases_id,
message_id: 100,
topic_name: "release notes",
});
// The narrowed channel's topics and other channels' topics each
// get their own match budget, so the strong match from another
// channel still surfaces; the narrowed channel's weak matches
// follow in recency order.
const suggestions = get_suggestions("topic:release notes");
const expected = [
`channel:${releases_id} topic:release+notes`,
"topic:release notes",
...Array.from(
{length: 10},
(_, i) => `channel:${narrow_stream_id} topic:old+release+notes+${10 - i}`,
),
];
assert.deepEqual(suggestions, expected);
});
test("people_suggestions", ({override}) => {
let query = "te";
override(narrow_state, "stream_id", noop);
const bob = {
email: "bob@zulip.com",
user_id: 202,
full_name: "Bob Térry",
avatar_url: example_avatar_url,
is_guest: false,
};
const alice = {
email: "alice@zulip.com",
user_id: 203,
full_name: "Alice Ignore",
};
people.add_active_user(ted, "server_events");
people.add_active_user(bob, "server_events");
people.add_active_user(alice, "server_events");
// Add an inaccessible user to verify that it is not included in
// suggestions.
const inaccessible_user = {
user_id: 299,
// All inaccessible users are named as "Unknown user", but we name
// it differently here so that the name matches the search query.
full_name: "Test unknown user",
email: "user299@zulipdev.com",
is_inaccessible_user: true,
};
people._add_user(inaccessible_user);
let suggestions = get_suggestions(query);
// Both match "te"; compare_users_for_dms called without query
// so no name-length tiebreaker — alphabetical by full_name.
let expected = [
"te",
`dm:${bob.user_id}`, // Bob Térry
`dm:${ted.user_id}`, // Ted Smith
`sender:${bob.user_id}`,
`sender:${ted.user_id}`,
`dm-including:${bob.user_id}`,
`dm-including:${ted.user_id}`,
`mentions:${bob.user_id}`,
`mentions:${ted.user_id}`,
];
assert.deepEqual(suggestions, expected);
const accessible_user = {
user_id: 299,
full_name: "Test unknown user",
email: "user299@zulipdev.com",
};
people.add_active_user(accessible_user, "server_events");
suggestions = get_suggestions(query);
// Same order with Test unknown user added (alphabetical).
expected = [
"te",
`dm:${bob.user_id}`, // Bob Térry
`dm:${ted.user_id}`, // Ted Smith
`dm:${inaccessible_user.user_id}`, // Test unknown user
`sender:${bob.user_id}`,
`sender:${ted.user_id}`,
`sender:${inaccessible_user.user_id}`,
`dm-including:${bob.user_id}`,
`dm-including:${ted.user_id}`,
`dm-including:${inaccessible_user.user_id}`,
`mentions:${bob.user_id}`,
`mentions:${ted.user_id}`,
`mentions:${inaccessible_user.user_id}`,
];
assert.deepEqual(suggestions, expected);
suggestions = get_suggestions("Ted "); // note space
expected = [
"Ted",
`dm:${ted.user_id}`,
`sender:${ted.user_id}`,
`dm-including:${ted.user_id}`,
`mentions:${ted.user_id}`,
];
assert.deepEqual(suggestions, expected);
query = "sender:ted sm";
expected = [`sender:${ted.user_id}`];
suggestions = get_suggestions(query);
assert.deepEqual(suggestions, expected);
// The search bar typeahead always converts person operators to
// user_id-based pills, so raw email operands are not reachable.
// Both valid and invalid emails get merged with the trailing
// search term, producing no person match.
query = "sender:ted@zulip.com new";
expected = [];
suggestions = get_suggestions(query);
assert.deepEqual(suggestions, expected);
query = "sender:ted@tulip.com new";
expected = [];
suggestions = get_suggestions(query);
assert.deepEqual(suggestions, expected);
});
test("operator_suggestions", ({override}) => {
override(stream_topic_history_util, "get_server_history", noop);
override(narrow_state, "stream_id", () => undefined);
// Completed operator should return nothing
let query = "channel:";
let suggestions = get_suggestions(query);
let expected = [];
assert.deepEqual(suggestions, expected);
query = "ch";
suggestions = get_suggestions(query);
expected = ["ch", "channels:", "channel:"];
assert.deepEqual(suggestions, expected);
query = "-s";
suggestions = get_suggestions(query);
expected = ["-s", "-sender:", "-channels:", "-channel:", `-sender:${me.user_id}`];
assert.deepEqual(suggestions, expected);
stream_data.add_sub_for_tests(
make_stream({
stream_id: 66,
name: "misc",
subscribed: true,
}),
);
query = "channel:66 is:alerted -f";
suggestions = get_suggestions(query);
expected = [
"channel:66 is:alerted -f",
"channel:66 is:alerted -sender:",
`channel:66 is:alerted -sender:${me.user_id}`,
];
assert.deepEqual(suggestions, expected);
});
test("queries_with_spaces", () => {
const office_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_id,
name: "office",
subscribed: true,
}),
);
const dev_help_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: dev_help_id,
name: "dev help",
subscribed: true,
}),
);
// test allowing spaces with quotes surrounding operand
let query = 'channel:"dev he"';
let suggestions = get_suggestions(query);
let expected = [`channel:${dev_help_id}`];
assert.deepEqual(suggestions, expected);
// test mismatched quote
query = 'channel:"dev h';
suggestions = get_suggestions(query);
expected = [`channel:${dev_help_id}`];
assert.deepEqual(suggestions, expected);
});
test("empty query suggestions in an inaccessible narrow", ({override}) => {
const office_id = new_stream_id();
stream_data.add_sub_for_tests(
make_stream({
stream_id: office_id,
name: "office",
subscribed: true,
}),
);
const unknown_stream_id = new_stream_id();
let narrow_terms;
override(narrow_state, "search_terms", () => narrow_terms);
override(narrow_state, "narrowed_by_topic_reply", () => false);
override(narrow_state, "narrowed_by_pm_reply", () => false);
// An empty query in an accessible narrow suggests searching the
// current conversation.
narrow_terms = [
{operator: "channel", operand: office_id.toString(), negated: false},
{operator: "topic", operand: "lunch", negated: false},
];
let suggestions = search.get_suggestions([], [], true);
assert.equal(suggestions[0], `channel:${office_id} topic:lunch`);
// When the narrow's channel is unknown to this client,
// we don't suggest searching the current conversation, and the
// suggestions are the same as if there were no current filter.
narrow_terms = [
{operator: "channel", operand: unknown_stream_id.toString(), negated: false},
{operator: "topic", operand: "lunch", negated: false},
];
suggestions = search.get_suggestions([], [], true);
assert.deepEqual(suggestions, search.get_suggestions([], [], false));
});