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
1607 lines
53 KiB
JavaScript
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));
|
|
});
|