',
};
const img_msg = {
// Even though the HTML class `message_inline_image` is modified
// in post_process for rendered message, the raw content stays the same.
content:
'
",
};
const clean_reactions_message = {
clean_reactions: new Map(
Object.entries({
"unicode_emoji,1f3b1": {
class: "message_reaction reacted",
count: 2,
emoji_alt_code: false,
emoji_code: "1f3b1",
emoji_name: "8ball",
is_realm_emoji: false,
label: "translated: You (click to remove) and Bob van Roberts reacted with :8ball:",
local_id: "unicode_emoji,1f3b1",
reaction_type: "unicode_emoji",
user_ids: [alice.user_id],
vote_text: "translated: You, Bob van Roberts",
},
}),
),
};
const non_reaction_msg = {
clean_reactions: new Map(),
};
predicate = get_predicate([["has", "non_valid_operand"]]);
assert.ok(!predicate(inline_audio_msg));
assert.ok(!predicate(inline_img_msg));
assert.ok(!predicate(img_msg));
assert.ok(!predicate(non_img_attachment_msg));
assert.ok(!predicate(link_msg));
assert.ok(!predicate(no_has_filter_matching_msg));
// HTML content of message is used to determine if image have link, image or attachment.
// We are using jquery to parse the html and find existence of relevant tags/elements.
// In tests we need to stub the calls to jquery so using zjquery's .set_find_results method.
function set_find_results_for_msg_content(msg, jquery_selector, results) {
$(`
${msg.content}
`).set_find_results(jquery_selector, results);
}
const has_link = get_predicate([["has", "link"]]);
set_find_results_for_msg_content(inline_audio_msg, "a", []);
assert.ok(!has_link(inline_audio_msg));
set_find_results_for_msg_content(inline_img_msg, "a", []);
assert.ok(!has_link(inline_img_msg));
set_find_results_for_msg_content(img_msg, "a", ["stub"]);
assert.ok(has_link(img_msg));
set_find_results_for_msg_content(non_img_attachment_msg, "a", ["stub"]);
assert.ok(has_link(non_img_attachment_msg));
set_find_results_for_msg_content(link_msg, "a", ["stub"]);
assert.ok(has_link(link_msg));
set_find_results_for_msg_content(no_has_filter_matching_msg, "a", []);
assert.ok(!has_link(no_has_filter_matching_msg));
const has_attachment = get_predicate([["has", "attachment"]]);
const attachment_selector_string =
"a[href^='/user_uploads'], img[src^='/user_uploads'], audio[src^='/user_uploads']";
set_find_results_for_msg_content(inline_audio_msg, attachment_selector_string, ["stub"]);
assert.ok(has_attachment(inline_audio_msg));
set_find_results_for_msg_content(inline_img_msg, attachment_selector_string, ["stub"]);
assert.ok(has_attachment(inline_img_msg));
set_find_results_for_msg_content(img_msg, attachment_selector_string, ["stub"]);
assert.ok(has_attachment(img_msg));
set_find_results_for_msg_content(non_img_attachment_msg, attachment_selector_string, ["stub"]);
assert.ok(has_attachment(non_img_attachment_msg));
set_find_results_for_msg_content(link_msg, attachment_selector_string, []);
assert.ok(!has_attachment(link_msg));
set_find_results_for_msg_content(no_has_filter_matching_msg, attachment_selector_string, []);
assert.ok(!has_attachment(no_has_filter_matching_msg));
const has_image = get_predicate([["has", "image"]]);
const image_selector_string = ".message_inline_image, .inline-image";
set_find_results_for_msg_content(inline_audio_msg, image_selector_string, []);
assert.ok(!has_image(inline_audio_msg));
set_find_results_for_msg_content(inline_img_msg, image_selector_string, ["stub"]);
assert.ok(has_image(inline_img_msg));
set_find_results_for_msg_content(img_msg, image_selector_string, ["stub"]);
assert.ok(has_image(img_msg));
set_find_results_for_msg_content(non_img_attachment_msg, image_selector_string, []);
assert.ok(!has_image(non_img_attachment_msg));
set_find_results_for_msg_content(link_msg, image_selector_string, []);
assert.ok(!has_image(link_msg));
set_find_results_for_msg_content(no_has_filter_matching_msg, image_selector_string, []);
assert.ok(!has_image(no_has_filter_matching_msg));
const has_reaction = get_predicate([["has", "reaction"]]);
assert.ok(has_reaction(clean_reactions_message));
assert.ok(!has_reaction(non_reaction_msg));
});
test("negated_predicates", () => {
let predicate;
let narrow;
const social_stream_id = new_stream_id();
make_sub("social", social_stream_id);
narrow = [{operator: "channel", operand: social_stream_id.toString(), negated: true}];
predicate = new Filter(narrow).predicate();
assert.ok(predicate({type: stream_message, stream_id: 999999}));
assert.ok(!predicate({type: stream_message, stream_id: social_stream_id}));
narrow = [{operator: "channels", operand: "public", negated: true}];
predicate = new Filter(narrow).predicate();
assert.ok(predicate({}));
});
test("predicate_edge_cases", () => {
let predicate;
// The code supports undefined as an operator to Filter, which results
// in a predicate that accepts any message.
predicate = new Filter([]).predicate();
assert.ok(predicate({}));
// Upstream code should prevent Filter.predicate from being called with
// invalid operator/operand combinations, but right now we just silently
// return a function that accepts all messages.
predicate = get_predicate([["in", "bogus"]]);
assert.ok(!predicate({}));
predicate = get_predicate([["is", "bogus"]]);
assert.ok(!predicate({}));
// Exercise caching feature.
const stream_id = new_stream_id();
make_sub("Off topic", stream_id);
const terms = [
{operator: "channel", operand: stream_id.toString()},
{operator: "topic", operand: "Mars"},
];
const filter = new Filter(terms);
filter.predicate();
predicate = filter.predicate(); // get cached version
assert.ok(predicate({type: stream_message, stream_id, topic: "Mars"}));
});
test("parse", () => {
let string;
let terms;
function _test(for_pills = false) {
const result = Filter.parse(string, for_pills);
assert_same_terms(result, terms);
}
make_sub(foo_sub.name, foo_stream_id);
string = "channel:Foo topic:bar yo";
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "search", operand: "yo"},
];
_test();
// A digit-leading channel name resolves by name, not by reading
// its leading digits as a stream id.
make_sub("7th", 7000);
string = "channel:7th";
terms = [{operator: "channel", operand: "7000"}];
_test();
// A digit-leading operand with no matching channel is left as-is,
// not coerced into a stream id.
string = "channel:9th";
terms = [{operator: "channel", operand: "9th"}];
_test();
string = `channel:${foo_stream_id} topic:bar yo`;
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "search", operand: "yo"},
];
_test();
// "stream" was renamed to "channel"
string = `stream:${foo_stream_id} topic:Bar yo`;
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "Bar"},
{operator: "search", operand: "yo"},
];
_test();
string = "dm:leo+test@zulip.com";
terms = [{operator: "dm", operand: "leo+test@zulip.com"}];
_test();
string = "sender:leo+test@zulip.com";
terms = [{operator: "sender", operand: "leo+test@zulip.com"}];
_test();
string = "sender:me";
terms = [{operator: "sender", operand: `${me.user_id}`}];
_test(true);
string = "-sender:me";
terms = [{operator: "sender", operand: `${me.user_id}`, negated: true}];
_test(true);
string = "mentions:me";
terms = [{operator: "is", operand: "mentioned"}];
_test(true);
string = "-mentions:me";
terms = [{operator: "is", operand: "mentioned", negated: true}];
_test(true);
string = "https://www.google.com";
terms = [{operator: "search", operand: "https://www.google.com"}];
_test();
string = `channel:${foo_stream_id} -channel:${invalid_sub_id}`;
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "channel", operand: invalid_sub_id.toString(), negated: true},
];
_test();
string = `text channel:${foo_stream_id} more text`;
terms = [
{operator: "search", operand: "text"},
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "search", operand: "more text"},
];
_test();
string = "text channels:public more text";
terms = [
{operator: "search", operand: "text"},
{operator: "channels", operand: "public"},
{operator: "search", operand: "more text"},
];
_test();
string = "channels:public";
terms = [{operator: "channels", operand: "public"}];
_test();
string = "-channels:public";
terms = [{operator: "channels", operand: "public", negated: true}];
_test();
// "streams" was renamed to "channels"
string = "streams:public";
terms = [{operator: "channels", operand: "public"}];
_test();
string = `channel:${foo_stream_id} :emoji: are cool`;
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "search", operand: ":emoji: are cool"},
];
_test();
string = `:channel: channel:${foo_stream_id} :emoji: are cool`;
terms = [
{operator: "search", operand: ":channel:"},
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "search", operand: ":emoji: are cool"},
];
_test();
string = `:channel: channel:${foo_stream_id} -:emoji: are cool`;
terms = [
{operator: "search", operand: ":channel:"},
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "search", operand: "-:emoji: are cool"},
];
_test();
string = "";
terms = [];
_test();
string = `channel: ${foo_stream_id} topic: "separated with space"`;
terms = [
{operator: "channel", operand: ""},
{operator: "search", operand: foo_stream_id.toString()},
{operator: "topic", operand: ""},
{operator: "search", operand: '"separated with space"'},
];
_test();
string = `topic: is:starred`;
terms = [
{operator: "topic", operand: ""},
{operator: "is", operand: "starred"},
];
_test();
string = "https://www.google.com";
terms = [{operator: "search", operand: "https://www.google.com"}];
_test();
string = "date:a week ago";
terms = [
{operator: "date", operand: "a"},
{operator: "search", operand: "week ago"},
];
_test();
string = "date:2025-03-12 hello";
terms = [
{operator: "date", operand: "2025-03-12"},
{operator: "search", operand: "hello"},
];
_test();
});
test("unparse", () => {
let string;
let terms;
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "Bar", negated: true},
{operator: "search", operand: "yo"},
];
string = `channel:${foo_stream_id} -topic:Bar yo`;
assert.deepEqual(Filter.unparse(terms), string);
terms = [
{operator: "channels", operand: "public"},
{operator: "search", operand: "text"},
];
string = "channels:public text";
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "channels", operand: "public"}];
string = "channels:public";
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "channels", operand: "public", negated: true}];
string = "-channels:public";
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "id", operand: "50"}];
string = "id:50";
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "near", operand: "150"}];
string = "near:150";
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "", operand: ""}];
string = "";
assert.deepEqual(Filter.unparse(terms), string);
// canonical version of the operator is
// used in the unparsed search string
terms = [
{operator: "stream", operand: foo_stream_id.toString()},
{operator: "subject", operand: "Bar"},
];
string = `channel:${foo_stream_id} topic:Bar`;
assert.deepEqual(Filter.unparse(terms), string);
terms = [{operator: "topic", operand: '\t "%+.\u{A0}'}];
string = "topic:%09+%22%25%2B.%C2%A0";
assert.equal(Filter.unparse(terms), string);
assert_same_terms(Filter.parse(string), terms);
});
test("describe", ({mock_template, override}) => {
let narrow;
let string;
let terms;
mock_template("search_description.hbs", true, (_data, html) => html);
narrow = [{operator: "channels", operand: "public"}];
string = "all public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "public", negated: true}];
string = "exclude all public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "archived"}];
string = "archived channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "archived", negated: true}];
string = "exclude archived channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public"}];
string = "all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public", negated: true}];
string = "exclude all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
page_params.is_spectator = true;
narrow = [{operator: "channels", operand: "public"}];
string = "all public channels that you can view";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public"}];
string = "all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "channels", operand: "web-public", negated: true}];
string = "exclude all web-public channels";
assert.equal(Filter.search_description_as_html(narrow, false), string);
page_params.is_spectator = false;
narrow = [{operator: "date", operand: ""}];
string = "messages sent around a specific date";
assert.equal(Filter.search_description_as_html(narrow, true), string);
narrow = [{operator: "date", operand: "2026-03-15"}];
string = "messages sent around March 15, 2026";
assert.equal(Filter.search_description_as_html(narrow, false), string);
const devel_decorated = `devel`;
const river_decorated = `river`;
const devel_id = new_stream_id();
make_sub("devel", devel_id);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "is", operand: "starred"},
];
string = `messages in ${devel_decorated}, starred messages`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
const river_id = new_stream_id();
make_sub("river", river_id);
narrow = [
{operator: "channel", operand: river_id.toString()},
{operator: "is", operand: "unread"},
];
string = `messages in ${river_decorated}, unread messages`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "topic", operand: "JS"},
];
string = `messages in ${devel_decorated} > JS`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "is", operand: "dm"},
{operator: "search", operand: "lunch"},
];
string = "direct messages, search for lunch";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "id", operand: "99"}];
string = "message ID 99";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "in", operand: "home"}];
string = "messages in home";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "mentioned"}];
string = "messages that mention you";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "alerted"}];
string = "alerted messages";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "resolved"}];
string = "resolved topics";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "followed"}];
string = "followed topics";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "muted"}];
string = "muted messages";
assert.equal(Filter.search_description_as_html(narrow, false), string);
// operands with their own negative words, like resolved.
narrow = [{operator: "is", operand: "resolved", negated: true}];
string = "unresolved topics";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "is", operand: "something_we_do_not_support"}];
string = "invalid something_we_do_not_support operand for is operator";
assert.equal(Filter.search_description_as_html(narrow, false), string);
// All the `is` operands are handled in search_suggestions. This is just
// for coverage.
assert.equal(Filter.operator_to_prefix("is"), "messages that are");
narrow = [{operator: "with", operand: "12"}];
string = "unknown operator";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "topic", operand: "JS", negated: true},
];
string = `messages in ${devel_decorated}, exclude topic JS`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "is", operand: "dm"},
{operator: "search", operand: "lunch", negated: true},
];
string = "direct messages, exclude lunch";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "is", operand: "starred", negated: true},
];
string = `messages in ${devel_decorated}, exclude starred messages`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "has", operand: "image", negated: true},
];
string = `messages in ${devel_decorated}, exclude messages with images`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "has", operand: "abc", negated: true},
{operator: "channel", operand: devel_id.toString()},
];
string = `invalid abc operand for has operator, messages in ${devel_decorated}`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "has", operand: "image", negated: true},
{operator: "channel", operand: devel_id.toString()},
];
string = `exclude messages with images, messages in ${devel_decorated}`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [];
string = "combined feed";
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "subject", operand: "JS", negated: true},
];
string = `messages in ${devel_decorated}, exclude topic JS`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
// Empty string topic involved.
override(realm, "realm_empty_topic_display_name", "general chat");
narrow = [
{operator: "channel", operand: devel_id.toString()},
{operator: "topic", operand: ""},
];
string = `messages in ${devel_decorated} > translated: general chat`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [
{operator: "topic", operand: ""},
{operator: "is", operand: "starred"},
];
string =
'topic translated: general chat, starred messages';
assert.equal(Filter.search_description_as_html(narrow, false), string);
narrow = [{operator: "topic", operand: ""}];
string = `topic translated: general chat`;
assert.equal(Filter.search_description_as_html(narrow, false), string);
/* Description for operator suggestion search terms */
terms = [{operator: "channels", operand: ""}];
string = "channel type";
assert.equal(Filter.search_description_as_html(terms, true), string);
terms = [{operator: "topic", operand: ""}];
string = "topic";
assert.equal(Filter.search_description_as_html(terms, true), string);
terms = [{operator: "near", operand: ""}];
string = "messages around";
assert.equal(Filter.search_description_as_html(terms, true), string);
terms = [{operator: "sender", operand: ""}];
string = "sent by";
assert.equal(Filter.search_description_as_html(terms, true), string);
terms = [{operator: "dm-including", operand: ""}];
string = "direct messages including";
assert.equal(Filter.search_description_as_html(terms, true), string);
terms = [{operator: "mentions", operand: ""}];
string = "messages mentioning";
assert.equal(Filter.search_description_as_html(terms, true), string);
});
test("can_bucket_by", () => {
const channel_id = new_stream_id();
make_sub("My channel", channel_id);
let terms = [{operator: "channel", operand: channel_id.toString()}];
let filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), true);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("dm"), false);
terms = [
// try a non-orthodox ordering
{operator: "topic", operand: "My topic"},
{operator: "channel", operand: channel_id.toString()},
];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), true);
assert.equal(filter.can_bucket_by("channel", "topic"), true);
assert.equal(filter.can_bucket_by("dm"), false);
terms = [
{operator: "channel", operand: channel_id.toString(), negated: true},
{operator: "topic", operand: "My topic"},
];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), false);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("dm"), false);
terms = [{operator: "dm", operand: [steve.user_id], negated: true}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), false);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("dm"), false);
assert.equal(filter.can_bucket_by("dm", "with"), false);
terms = [{operator: "dm", operand: [steve.user_id, joe.user_id]}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), false);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("dm"), true);
assert.equal(filter.can_bucket_by("dm", "with"), false);
assert.equal(filter.can_bucket_by("is-mentioned"), false);
assert.equal(filter.can_bucket_by("is-dm"), false);
terms = [
{operator: "dm", operand: [steve.user_id, joe.user_id]},
{operator: "with", operand: "7"},
];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel"), false);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("dm", "with"), true);
assert.equal(filter.can_bucket_by("is-mentioned"), false);
assert.equal(filter.can_bucket_by("is-dm"), false);
terms = [{operator: "is", operand: "dm"}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("is-mentioned"), false);
assert.equal(filter.can_bucket_by("is-dm"), true);
terms = [{operator: "is", operand: "mentioned"}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("is-mentioned"), true);
assert.equal(filter.can_bucket_by("is-dm"), false);
terms = [
{operator: "is", operand: "mentioned"},
{operator: "is", operand: "starred"},
];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("is-mentioned"), true);
assert.equal(filter.can_bucket_by("is-dm"), false);
// The call below returns false for somewhat arbitrary
// reasons -- we say is-dm has precedence over
// is-starred.
assert.equal(filter.can_bucket_by("is-starred"), false);
terms = [{operator: "is", operand: "mentioned", negated: true}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("is-mentioned"), false);
assert.equal(filter.can_bucket_by("is-dm"), false);
terms = [{operator: "is", operand: "resolved"}];
filter = new Filter(terms);
assert.equal(filter.can_bucket_by("channel", "topic"), false);
assert.equal(filter.can_bucket_by("channel"), false);
assert.equal(filter.can_bucket_by("dm"), false);
assert.equal(filter.can_bucket_by("is-mentioned"), false);
assert.equal(filter.can_bucket_by("is-dm"), false);
});
test("term_type", () => {
function assert_term_type(term, expected_term_type) {
assert.equal(Filter.term_type(term), expected_term_type);
}
function term(operator, operand, negated) {
return {
operator,
operand,
negated,
};
}
assert_term_type(term("channels", "public"), "channels-public");
assert_term_type(term("channel", "whatever"), "channel");
assert_term_type(term("dm", "whomever"), "dm");
assert_term_type(term("dm", "whomever", true), "not-dm");
assert_term_type(term("is", "dm"), "is-dm");
assert_term_type(term("is", "private"), "is-private");
assert_term_type(term("has", "link"), "has-link");
assert_term_type(term("has", "attachment", true), "not-has-attachment");
function assert_term_sort(in_terms, expected) {
const sorted_terms = Filter.sorted_term_types(in_terms);
assert.deepEqual(sorted_terms, expected);
}
assert_term_sort(["topic", "channel", "sender"], ["channel", "topic", "sender"]);
assert_term_sort(
["has-link", "near", "is-unread", "dm"],
["dm", "near", "is-unread", "has-link"],
);
assert_term_sort(["topic", "channel", "with"], ["channel", "topic", "with"]);
assert_term_sort(["bogus", "channel", "topic"], ["channel", "topic", "bogus"]);
assert_term_sort(["channel", "topic", "channel"], ["channel", "channel", "topic"]);
assert_term_sort(["search", "channels-public"], ["channels-public", "search"]);
const terms = [
{operator: "topic", operand: "lunch"},
{operator: "sender", operand: steve.user_id},
{operator: "channel", operand: new_stream_id().toString()},
];
let filter = new Filter(terms);
const term_types = filter.sorted_term_types();
assert.deepEqual(term_types, ["channel", "topic", "sender"]);
// test caching of term types
// init and stub
filter = new Filter(terms);
filter.stub = filter._build_sorted_term_types;
filter._build_sorted_term_types = function () {
this._build_sorted_term_types_called = true;
return this.stub();
};
// uncached trial
filter._build_sorted_term_types_called = false;
const built_terms = filter.sorted_term_types();
assert.deepEqual(built_terms, ["channel", "topic", "sender"]);
assert.ok(filter._build_sorted_term_types_called);
// cached trial
filter._build_sorted_term_types_called = false;
const cached_terms = filter.sorted_term_types();
assert.deepEqual(cached_terms, ["channel", "topic", "sender"]);
assert.ok(!filter._build_sorted_term_types_called);
});
test("first_valid_id_from", () => {
const terms = [{operator: "is", operand: "alerted"}];
const filter = new Filter(terms);
const messages = [
{
message: {
id: 5,
alerted: true,
},
},
{
message: {
id: 12,
},
},
{
message: {id: 20, alerted: true},
},
{
message: {id: 30, type: stream_message},
},
{
message: {id: 40, alerted: false},
},
];
message_store.set_messages_for_tests(messages);
const msg_ids = [10, 20, 30, 40];
assert.equal(filter.first_valid_id_from([999]), undefined);
assert.equal(filter.first_valid_id_from(msg_ids), 20);
});
test("convert_suggestion_to_term", () => {
const denmark = {
stream_id: 100,
name: "Denmark",
};
stream_data.add_sub_for_tests(denmark);
const test_data = [
["has:image", true],
["has:nonsense", false],
["is:unread", true],
["is:nonsense", false],
["in:home", true],
["in:nowhere", false],
["id:4", true],
["-id:4", false],
["near:1", true],
["-near:1", false],
["with:1", true],
["-with:1", false],
["near:home", false],
["channel:" + denmark.stream_id, true],
[`channel:${invalid_sub_id}`, false],
["channels:public", true],
["channels:private", false],
["topic:GhostTown", true],
[`dm-including:${alice.user_id}`, true],
["sender:-1", false],
["sender:me", true],
[`dm:${[alice.user_id, -1]}`, false],
[`dm:${[alice.user_id, joe.user_id]}`, true],
[`mentions:${alice.user_id}`, true],
["mentions:me", true],
["mentions:-1", false],
["mentions:invalid", false],
// A typed full name resolves to a user id, matching the help center
// docs. Quotes keep a multi-word name a single operand through the
// parser. A third tuple element pins down the resolved term, so a
// name maps to the right user id and not merely to some valid term.
[`sender:"${karl.full_name}"`, true, {operator: "sender", operand: karl.user_id}],
[`mentions:"${karl.full_name}"`, true, {operator: "mentions", operand: karl.user_id}],
// dm and dm-including resolve a comma-separated list of names to the
// corresponding user ids.
[`dm:"${karl.full_name}"`, true, {operator: "dm", operand: [karl.user_id]}],
[
`dm:"${karl.full_name}, ${shubham.full_name}"`,
true,
{operator: "dm", operand: [karl.user_id, shubham.user_id]},
],
[
`dm-including:"${karl.full_name}, ${shubham.full_name}"`,
true,
{operator: "dm-including", operand: [karl.user_id, shubham.user_id]},
],
// A name that matches no user is invalid.
['dm:"No Such Person"', false],
// An ambiguous (duplicate) full name is invalid on its own...
[`dm:"${sam_one.full_name}"`, false],
// ...but the `Full Name|user_id` form disambiguates it, resolving to
// exactly the requested user and not the other duplicate.
[
`dm:"${sam_one.full_name}|${sam_one.user_id}"`,
true,
{operator: "dm", operand: [sam_one.user_id]},
],
[
`dm:"${sam_two.full_name}|${sam_two.user_id}"`,
true,
{operator: "dm", operand: [sam_two.user_id]},
],
[`-date:2022-01-01`, false],
[`date:2022-01-01`, true],
// Not a valid date string.
[`date:2022-01-01abcd`, false],
// In the past
[`date:1500-01-06`, false],
// Not in the `yyyy-MM-dd` format
[`date:2020-`, false],
];
const today = new Date(2024, 0, 15, 2, 0, 0);
clock.setSystemTime(today.getTime());
for (const [search_term_string, expected_is_valid, expected_term] of test_data) {
const term = Filter.convert_suggestion_to_term(Filter.parse(search_term_string)[0]);
assert.equal(term !== undefined, expected_is_valid);
if (expected_term !== undefined) {
assert.deepEqual(term, {negated: false, ...expected_term});
}
}
clock.reset();
// Invalid operator.
assert.equal(
Filter.convert_suggestion_to_term({
operator: "foo",
operand: "bar",
}) !== undefined,
false,
);
});
test("try_adjusting_for_moved_with_target", () => {
const scotland_id = new_stream_id();
make_sub("Scotland", scotland_id);
const verona_id = new_stream_id();
make_sub("Verona", verona_id);
const messages = [
{
message: {
type: "stream",
stream_id: scotland_id,
display_recipient: "Scotland",
topic: "Test 1",
id: 12,
},
},
{
message: {
type: "stream",
stream_id: verona_id,
display_recipient: "Verona",
topic: "Test 2",
id: 17,
},
},
{
message: {
type: "direct",
id: 2,
display_recipient: [{id: 3, email: "user3@zulip.com"}],
},
},
];
message_store.set_messages_for_tests(messages);
// When the narrow terms are correct, it returns the same terms
let terms = [
{operator: "channel", operand: scotland_id.toString(), negated: false},
{operator: "topic", operand: "Test 1", negated: false},
{operator: "with", operand: "12", negated: false},
];
let filter = new Filter(terms);
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, true);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, false);
assert.deepEqual(filter.terms(), terms);
// When the narrow terms are incorrect, the narrow is corrected
// to the narrow of the `with` operand.
const incorrect_terms = [
{operator: "channel", operand: verona_id.toString(), negated: false},
{operator: "topic", operand: "Test 2", negated: false},
{operator: "with", operand: "12", negated: false},
];
filter = new Filter(incorrect_terms);
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, true);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, false);
assert.deepEqual(filter.terms(), terms);
// when message specified in `with` operator does not exist in
// message_store, we rather go to the server, without any updates.
terms = [
{operator: "channel", operand: scotland_id.toString(), negated: false},
{operator: "topic", operand: "Test 1", negated: false},
{operator: "with", operand: "11", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, true);
assert.deepEqual(filter.terms(), terms);
// When the narrow consists of `channel` or `topic` operators, while
// the `with` operator corresponds to that of a direct message, then
// the narrow is adjusted to point to the narrow containing the message.
terms = [
{operator: "channel", operand: scotland_id.toString(), negated: false},
{operator: "topic", operand: "Test 1", negated: false},
{operator: "with", operand: "2", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, false);
assert.deepEqual(filter.terms(), [
{operator: "dm", operand: [3], negated: false},
{operator: "with", operand: "2", negated: false},
]);
// When the narrow consists of `dm` operators, while the `with`
// operator corresponds to that of a channel topic message.
terms = [
{operator: "dm", operand: [alice.user_id]},
{operator: "with", operand: "12"},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.requires_adjustment_for_moved_with_target, false);
assert.deepEqual(filter.terms(), [
{operator: "channel", operand: scotland_id.toString(), negated: false},
{operator: "topic", operand: "Test 1", negated: false},
{operator: "with", operand: "12", negated: false},
]);
// When message id attached to `with` operator is found locally,
// and is present in the same narrow as the original one, then
// no hash change is required.
terms = [
{operator: "channel", operand: verona_id.toString(), negated: false},
{operator: "topic", operand: "Test 2", negated: false},
{operator: "with", operand: "17", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.narrow_requires_hash_change, false);
// When message id attached to `with` operator is not found
// locally, but messages fetched are in same narrow as
// original narrow, then no hash change is required.
terms = [
{operator: "channel", operand: verona_id.toString(), negated: false},
{operator: "topic", operand: "Test 2", negated: false},
{operator: "with", operand: "1", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
// now messages are fetched from server, and a single
// fetched message is used to adjust narrow terms.
filter.try_adjusting_for_moved_with_target(message_store.get(17));
assert.deepEqual(filter.narrow_requires_hash_change, false);
// When message id attached to `with` operator is found locally,
// and is not present in the same narrow as the original one,
// then hash change is required.
terms = [
{operator: "channel", operand: verona_id.toString(), negated: false},
{operator: "topic", operand: "Test 2", negated: false},
{operator: "with", operand: "12", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
assert.deepEqual(filter.narrow_requires_hash_change, true);
// When message id attached to `with` operator is not found
// locally, and messages fetched are in different narrow from
// original narrow, then hash change is required.
terms = [
{operator: "channel", operand: verona_id.toString(), negated: false},
{operator: "topic", operand: "Test 2", negated: false},
{operator: "with", operand: "1", negated: false},
];
filter = new Filter(terms);
filter.try_adjusting_for_moved_with_target();
// now messages are fetched from server, and a single
// fetched message is used to adjust narrow terms.
filter.try_adjusting_for_moved_with_target(message_store.get(12));
assert.deepEqual(filter.narrow_requires_hash_change, true);
});
function make_private_sub(name, stream_id) {
const sub = {
name,
stream_id,
invite_only: true,
};
stream_data.add_sub_for_tests(sub);
}
function make_web_public_sub(name, stream_id) {
const sub = {
name,
stream_id,
is_web_public: true,
};
stream_data.add_sub_for_tests(sub);
}
function make_archived_sub(name, stream_id) {
const sub = {
name,
stream_id,
is_archived: true,
};
stream_data.add_sub_for_tests(sub);
}
test("navbar_helpers", ({override}) => {
stream_data.add_sub_for_tests(foo_sub);
// make sure title has names separated with correct delimiters
function properly_separated_names(names) {
const names_internationalized = new Intl.ListFormat("en", {
style: "long",
type: "conjunction",
}).format(names);
return names_internationalized;
}
function test_redirect_url_with_search(test_case) {
const terms = [...test_case.terms, {operator: "search", operand: "fizzbuzz"}];
const filter = new Filter(terms);
assert.equal(filter.generate_redirect_url(), test_case.redirect_url_with_search);
}
function test_common_narrow(test_case) {
const filter = new Filter(test_case.terms);
assert.equal(filter.is_common_narrow(), test_case.is_common_narrow);
}
function test_add_icon_data(test_case) {
const filter = new Filter(test_case.terms);
let context = {};
context = filter.add_icon_data(context);
assert.equal(context.icon, test_case.icon);
assert.equal(context.zulip_icon, test_case.zulip_icon);
}
function test_get_title(test_case) {
const filter = new Filter(test_case.terms);
assert.deepEqual(filter.get_title(), test_case.title);
}
function test_get_description(test_case) {
const filter = new Filter(test_case.terms);
const description = filter.get_description();
if (test_case.description !== undefined && test_case.link !== undefined) {
assert.deepEqual(description, {
description: test_case.description,
link: test_case.link,
});
} else {
assert.strictEqual(description, undefined);
}
}
function test_helpers(test_case) {
// debugging tip: add a `console.log(test_case)` here
test_common_narrow(test_case);
test_add_icon_data(test_case);
test_get_title(test_case);
test_get_description(test_case);
test_redirect_url_with_search(test_case);
}
const sender = [{operator: "sender", operand: joe.user_id}];
const guest_sender = [{operator: "sender", operand: alice.user_id}];
const invalid_sender = [{operator: "sender", operand: 9999}];
const in_home = [{operator: "in", operand: "home"}];
const in_all = [{operator: "in", operand: "all"}];
const is_starred = [{operator: "is", operand: "starred"}];
const is_dm = [{operator: "is", operand: "dm"}];
const not_is_dm = [{operator: "is", operand: "dm", negated: true}];
const is_mentioned = [{operator: "is", operand: "mentioned"}];
const is_resolved = [{operator: "is", operand: "resolved"}];
const is_followed = [{operator: "is", operand: "followed"}];
const channels_public = [{operator: "channels", operand: "public"}];
const channels_archived = [{operator: "channels", operand: "archived"}];
const not_channels_archived = [{operator: "channels", operand: "archived", negated: true}];
const channels_web_public = [{operator: "channels", operand: "web-public"}];
const channel_topic_terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
];
const has_reaction_sender_me = [
{operator: "has", operand: "reaction"},
{operator: "sender", operand: me.user_id},
];
// foo channel exists
const channel_term = [{operator: "channel", operand: foo_stream_id.toString()}];
const invalid_channel_id = new_stream_id();
const invalid_channel = [{operator: "channel", operand: invalid_channel_id.toString()}];
const invalid_channel_with_topic = [
{operator: "channel", operand: invalid_channel_id.toString()},
{operator: "topic", operand: "bar"},
];
// channel/topic name using special character for url encoding.
const special_sub_id = new_stream_id();
make_sub("Foo2.0", special_sub_id);
const char_channel_term = [{operator: "channel", operand: special_sub_id.toString()}];
const char_channel_topic_term = [
{operator: "channel", operand: special_sub_id.toString()},
{operator: "topic", operand: "bar2.0"},
];
const public_sub_id = new_stream_id();
make_private_sub("psub", public_sub_id);
const private_channel_term = [{operator: "channel", operand: public_sub_id.toString()}];
const web_public_sub_id = new_stream_id();
make_web_public_sub("webPublicSub", web_public_sub_id); // capitalized just to try be tricky and robust.
const web_public_channel = [{operator: "channel", operand: web_public_sub_id.toString()}];
const archived_sub_id = new_stream_id();
make_archived_sub("archivedSub", archived_sub_id);
const archived_channel_term = [{operator: "channel", operand: archived_sub_id.toString()}];
const dm = [{operator: "dm", operand: [joe.user_id]}];
const dm_with = [
{operator: "dm", operand: [joe.user_id]},
{operator: "with", operand: "12"},
];
const dm_with_self = [{operator: "dm", operand: [me.user_id]}];
const dm_group = [{operator: "dm", operand: [joe.user_id, steve.user_id]}];
const dm_with_guest = [{operator: "dm", operand: [alice.user_id]}];
const dm_with_muted_user = [{operator: "dm", operand: [jeff.user_id]}];
const dm_with_muted_guest_user = [{operator: "dm", operand: [annie.user_id]}];
const dm_group_including_guest = [{operator: "dm", operand: [joe.user_id, alice.user_id]}];
const dm_group_including_muted_user = [{operator: "dm", operand: [joe.user_id, jeff.user_id]}];
const dm_group_including_muted_guest_user = [
{operator: "dm", operand: [joe.user_id, annie.user_id]},
];
const dm_group_including_missing_person = [{operator: "dm", operand: [9999, joe.user_id]}];
// not common narrows, but used for browser title updates
const is_alerted = [{operator: "is", operand: "alerted"}];
const is_unread = [{operator: "is", operand: "unread"}];
const channel_topic_near = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "near", operand: "12"},
];
const dm_near = [
{operator: "dm", operand: [joe.user_id]},
{operator: "near", operand: "12"},
];
const channel_with = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "with", operand: "12"},
];
const test_cases = [
{
terms: sender,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages sent by " + joe.full_name,
redirect_url_with_search: "/#narrow/sender/" + joe.user_id + "-joe",
},
{
terms: guest_sender,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages sent by translated: alice (guest)",
redirect_url_with_search: "/#narrow/sender/" + alice.user_id + "-alice",
},
{
terms: invalid_sender,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages sent by unknown user",
redirect_url_with_search: "/#narrow/sender/undefined",
},
{
terms: is_starred,
is_common_narrow: true,
zulip_icon: "star",
title: "translated: Starred messages",
redirect_url_with_search: "/#narrow/is/starred",
description: "translated: Important messages, tasks, and other useful references.",
link: "/help/star-a-message#view-your-starred-messages",
},
{
terms: in_home,
is_common_narrow: true,
icon: "home",
title: "translated: Combined feed",
redirect_url_with_search: "#",
},
{
terms: in_all,
is_common_narrow: true,
icon: "home",
title: "translated: All messages including muted channels",
redirect_url_with_search: "#",
},
{
terms: is_dm,
is_common_narrow: true,
zulip_icon: "user",
title: "translated: Direct message feed",
redirect_url_with_search: "/#narrow/is/dm",
},
{
terms: not_is_dm,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "translated: Channel messages",
redirect_url_with_search: "/#narrow/-is/dm",
},
{
terms: is_mentioned,
is_common_narrow: true,
zulip_icon: "at-sign",
title: "translated: Mentions",
redirect_url_with_search: "/#narrow/is/mentioned",
description: "translated: Messages where you are mentioned.",
link: "/help/view-your-mentions",
},
{
terms: is_resolved,
is_common_narrow: true,
icon: "check",
title: "translated: Resolved topics",
redirect_url_with_search: "/#narrow/topics/is/resolved",
},
{
terms: is_followed,
is_common_narrow: true,
zulip_icon: "follow",
title: "translated: Followed topics",
redirect_url_with_search: "/#narrow/topics/is/followed",
description: "translated: Messages in topics you follow.",
link: "/help/follow-a-topic",
},
{
terms: channel_topic_terms,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "Foo",
redirect_url_with_search: `/#narrow/channel/${foo_stream_id}-Foo/topic/bar`,
},
{
terms: char_channel_term,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "Foo2.0",
redirect_url_with_search: `/#narrow/channel/${special_sub_id}-Foo2.2E0`,
},
{
terms: char_channel_topic_term,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "Foo2.0",
redirect_url_with_search: `/#narrow/channel/${special_sub_id}-Foo2.2E0/topic/bar2.2E0`,
},
{
terms: invalid_channel_with_topic,
is_common_narrow: true,
icon: "question-circle-o",
title: "translated: Unknown channel",
redirect_url_with_search: "#",
},
{
terms: channels_public,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages in all public channels",
redirect_url_with_search: "/#narrow/channels/public",
},
{
terms: channels_archived,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages in your archived channels",
redirect_url_with_search: "/#narrow/channels/archived",
},
{
terms: not_channels_archived,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages not in archived channels",
redirect_url_with_search: "/#narrow/-channels/archived",
},
{
terms: channels_web_public,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages in all web-public channels",
redirect_url_with_search: "/#narrow/channels/web-public",
},
{
terms: channel_term,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "Foo",
redirect_url_with_search: `/#narrow/channel/${foo_stream_id}-Foo`,
},
{
terms: invalid_channel,
is_common_narrow: true,
icon: "question-circle-o",
title: "translated: Unknown channel",
redirect_url_with_search: "#",
},
{
terms: private_channel_term,
is_common_narrow: true,
zulip_icon: "lock",
title: "psub",
redirect_url_with_search: `/#narrow/channel/${public_sub_id}-psub`,
},
{
terms: web_public_channel,
is_common_narrow: true,
zulip_icon: "globe",
title: "webPublicSub",
redirect_url_with_search: `/#narrow/channel/${web_public_sub_id}-webPublicSub`,
},
{
terms: archived_channel_term,
is_common_narrow: true,
zulip_icon: "archive",
title: "archivedSub",
redirect_url_with_search: `/#narrow/channel/${archived_sub_id}-archivedSub`,
},
{
terms: dm,
is_common_narrow: true,
zulip_icon: "user",
title: properly_separated_names([joe.full_name]),
redirect_url_with_search:
"/#narrow/dm/" + joe.user_id + "-" + parseOneAddress(joe.email).local,
},
{
terms: dm_group,
is_common_narrow: true,
zulip_icon: "user",
title: properly_separated_names([joe.full_name, steve.full_name]),
redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + steve.user_id + "-group",
},
{
terms: dm_with_muted_user,
is_common_narrow: true,
zulip_icon: "user",
title: "translated: Muted user",
redirect_url_with_search: "/#narrow/dm/" + jeff.user_id + "-" + jeff.full_name,
},
{
terms: dm_with_muted_guest_user,
is_common_narrow: true,
zulip_icon: "user",
title: "translated: Muted user (guest)",
redirect_url_with_search: "/#narrow/dm/" + annie.user_id + "-" + annie.full_name,
},
{
terms: dm_with_guest,
is_common_narrow: true,
zulip_icon: "user",
title: "translated: alice (guest)",
redirect_url_with_search:
"/#narrow/dm/" + alice.user_id + "-" + parseOneAddress(alice.email).local,
},
{
terms: dm_group_including_guest,
is_common_narrow: true,
zulip_icon: "user",
title: "joe and translated: alice (guest)",
redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + alice.user_id + "-group",
},
{
terms: dm_group_including_muted_user,
is_common_narrow: true,
zulip_icon: "user",
title: "joe and translated: Muted user",
redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + jeff.user_id + "-group",
},
{
terms: dm_group_including_muted_guest_user,
is_common_narrow: true,
zulip_icon: "user",
title: "joe and translated: Muted user (guest)",
redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + annie.user_id + "-group",
},
{
terms: dm_group_including_missing_person,
is_common_narrow: true,
zulip_icon: "user",
title: "joe and translated: Unknown user (9999)",
redirect_url_with_search: "/#narrow/dm/undefined",
},
{
terms: has_reaction_sender_me,
is_common_narrow: true,
zulip_icon: "smile",
title: "translated: Reactions",
redirect_url_with_search: "/#narrow/has/reaction/sender/me",
description: "translated: Emoji reactions to your messages.",
link: "/help/emoji-reactions",
},
{
terms: is_alerted,
is_common_narrow: false,
icon: undefined,
title: "translated: Alerted messages",
redirect_url_with_search: "#",
},
{
terms: is_unread,
is_common_narrow: false,
icon: undefined,
title: "translated: Unread messages",
redirect_url_with_search: "#",
},
{
terms: channel_topic_near,
is_common_narrow: false,
zulip_icon: "hashtag",
title: "Foo",
redirect_url_with_search: "#",
},
{
terms: dm_near,
is_common_narrow: false,
zulip_icon: "user",
title: properly_separated_names([joe.full_name]),
redirect_url_with_search: "#",
},
{
terms: channel_with,
is_common_narrow: true,
zulip_icon: "hashtag",
title: "Foo",
redirect_url_with_search: "#",
},
{
terms: dm_with,
is_common_narrow: true,
zulip_icon: "user",
title: properly_separated_names([joe.full_name]),
redirect_url_with_search: "#",
},
{
terms: dm_with_self,
is_common_narrow: true,
zulip_icon: "user",
title: "translated: Messages with yourself",
redirect_url_with_search: "/#narrow/dm/30-Me-Myself",
},
];
override(realm, "realm_enable_guest_user_indicator", true);
for (const test_case of test_cases) {
test_helpers(test_case);
}
// TODO: these may be removed, based on design decisions
const sender_me = [{operator: "sender", operand: me.user_id}];
const sender_joe = [{operator: "sender", operand: joe.user_id}];
const redirect_edge_cases = [
{
terms: sender_me,
redirect_url_with_search: "/#narrow/sender/" + me.user_id + "-Me-Myself",
is_common_narrow: true,
},
{
terms: sender_joe,
redirect_url_with_search: "/#narrow/sender/" + joe.user_id + "-joe",
is_common_narrow: true,
},
];
for (const test_case of redirect_edge_cases) {
test_redirect_url_with_search(test_case);
}
// TODO: test every single one of the "ALL" redirects from the navbar behaviour table
// incomplete and weak test cases just to restore coverage of filter.ts
const complex_term = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "sender", operand: me.user_id},
];
const redirect_url = "#";
let filter = new Filter(complex_term);
assert.equal(filter.generate_redirect_url(), redirect_url);
assert.equal(filter.is_common_narrow(), false);
const channel_topic_search_term = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
{operator: "search", operand: "potato"},
];
const channel_topic_search_term_test_case = {
terms: channel_topic_search_term,
title: undefined,
};
test_get_title(channel_topic_search_term_test_case);
page_params.is_spectator = true;
const channels_public_search_test_case_for_spectator = {
terms: channels_public,
title: "translated: Messages in all public channels that you can view",
};
test_get_title(channels_public_search_test_case_for_spectator);
page_params.is_spectator = false;
override(realm, "realm_enable_guest_user_indicator", false);
const guest_user_test_cases_without_indicator = [
{
terms: guest_sender,
is_common_narrow: true,
icon: undefined,
title: "translated: Messages sent by alice",
redirect_url_with_search: "/#narrow/sender/" + alice.user_id + "-alice",
},
{
terms: dm_with_guest,
is_common_narrow: true,
zulip_icon: "user",
title: properly_separated_names([alice.full_name]),
redirect_url_with_search:
"/#narrow/dm/" + alice.user_id + "-" + parseOneAddress(alice.email).local,
},
{
terms: dm_group_including_guest,
is_common_narrow: true,
zulip_icon: "user",
title: properly_separated_names([alice.full_name, joe.full_name]),
redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + alice.user_id + "-group",
},
];
for (const test_case of guest_user_test_cases_without_indicator) {
test_helpers(test_case);
}
// this is actually wrong, but the code is currently not robust enough to throw an error here
// also, used as an example of triggering last return statement.
const default_redirect = {
terms: [{operator: "channel", operand: foo_stream_id.toString()}],
redirect_url: "#",
};
filter = new Filter(default_redirect.terms);
assert.equal(filter.generate_redirect_url(), default_redirect.redirect_url);
});
test("error_cases", () => {
// This test just gives us 100% line coverage on defensive code that
// should not be reached unless we break other code.
const predicate = get_predicate([["dm", [joe.user_id]]]);
blueslip.expect("error", "Empty recipient list in message");
assert.ok(!predicate({type: direct_message, display_recipient: []}));
});
run_test("is_spectator_compatible", () => {
// tests same as test_is_spectator_compatible from test_message_fetch.py
assert.ok(Filter.is_spectator_compatible([]));
assert.ok(Filter.is_spectator_compatible([{operator: "is", operand: "resolved"}]));
assert.ok(
Filter.is_spectator_compatible([{operator: "is", operand: "resolved", negated: true}]),
);
assert.ok(Filter.is_spectator_compatible([{operator: "has", operand: "attachment"}]));
assert.ok(Filter.is_spectator_compatible([{operator: "has", operand: "image"}]));
assert.ok(Filter.is_spectator_compatible([{operator: "search", operand: "magic"}]));
assert.ok(Filter.is_spectator_compatible([{operator: "near", operand: "15"}]));
assert.ok(
Filter.is_spectator_compatible([
{operator: "id", operand: "15"},
{operator: "has", operand: "attachment"},
]),
);
assert.ok(Filter.is_spectator_compatible([{operator: "sender", operand: "hamlet@zulip.com"}]));
assert.ok(!Filter.is_spectator_compatible([{operator: "dm", operand: "hamlet@zulip.com"}]));
assert.ok(
!Filter.is_spectator_compatible([{operator: "dm-including", operand: "hamlet@zulip.com"}]),
);
const denmark_id = new_stream_id();
make_sub("Denmark", denmark_id);
assert.ok(
Filter.is_spectator_compatible([{operator: "channel", operand: denmark_id.toString()}]),
);
assert.ok(
Filter.is_spectator_compatible([
{operator: "channel", operand: denmark_id.toString()},
{operator: "topic", operand: "logic"},
]),
);
assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "starred"}]));
assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "dm"}]));
assert.ok(Filter.is_spectator_compatible([{operator: "channels", operand: "public"}]));
assert.ok(Filter.is_spectator_compatible([{operator: "channels", operand: "archived"}]));
// Malformed input not allowed
assert.ok(!Filter.is_spectator_compatible([{operator: "has"}]));
// "is:private" was renamed to "is:dm"
assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "private"}]));
// "pm-with" was renamed to "dm"
assert.ok(
!Filter.is_spectator_compatible([{operator: "pm-with", operand: "hamlet@zulip.com"}]),
);
// "stream" was renamted to "channel"
assert.ok(Filter.is_spectator_compatible([{operator: "stream", operand: "Denmark"}]));
// "streams" was renamed to "channels"
assert.ok(Filter.is_spectator_compatible([{operator: "streams", operand: "public"}]));
// "group-pm-with:" was replaced with "dm-including:"
assert.ok(
!Filter.is_spectator_compatible([{operator: "group-pm-with", operand: "hamlet@zulip.com"}]),
);
assert.ok(
!Filter.is_spectator_compatible([{operator: "mentions", operand: "hamlet@zulip.com"}]),
);
});
run_test("is_in_home", () => {
const filter = new Filter([{operator: "in", operand: "home"}]);
assert.ok(filter.is_in_home());
const filter2 = new Filter([{operator: "in", operand: "all"}]);
assert.ok(!filter2.is_in_home());
// Test home with additional terms is not all messages.
const filter3 = new Filter([
{operator: "in", operand: "home"},
{operator: "topic", operand: "foo"},
]);
assert.ok(!filter3.is_in_home());
});
run_test("excludes_muted_topics", () => {
let filter = new Filter([{operator: "is", operand: "starred"}]);
assert.ok(!filter.excludes_muted_topics());
filter = new Filter([{operator: "is", operand: "muted"}]);
assert.ok(!filter.excludes_muted_topics());
filter = new Filter([{operator: "in", operand: "home", negated: true}]);
assert.ok(!filter.excludes_muted_topics());
filter = new Filter([{operator: "search", operand: "pizza"}]);
assert.ok(!filter.excludes_muted_topics());
filter = new Filter([
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "bar"},
]);
assert.ok(!filter.excludes_muted_topics());
filter = new Filter([{operator: "is", operand: "dm"}]);
assert.ok(!filter.excludes_muted_topics());
});
run_test("equals", () => {
let terms = [{operator: "channel", operand: foo_stream_id.toString()}];
let filter = new Filter(terms);
assert.ok(filter.equals(new Filter(terms)));
assert.ok(!filter.equals(new Filter([])));
assert.ok(!filter.equals(new Filter([{operand: "Bar", operator: "channel"}])));
assert.ok(!filter.equals(new Filter([...terms, {operator: "topic", operand: "Bar"}])));
terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "topic", operand: "Bar"},
];
filter = new Filter(terms);
assert.ok(
filter.equals(
new Filter([
{operator: "topic", operand: "Bar"},
{operator: "channel", operand: foo_stream_id.toString()},
]),
),
);
assert.ok(!filter.equals(new Filter([...terms, {operator: "near", operand: "10"}])));
// Exclude `near` operator from comparison.
assert.ok(filter.equals(new Filter([...terms, {operator: "near", operand: "10"}]), ["near"]));
assert.ok(
filter.equals(
new Filter([
{operator: "near", operand: "10"},
{operator: "topic", operand: "Bar"},
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "near", operand: "101"},
]),
["near"],
),
);
});
run_test("adjusted_terms_if_moved", ({override}) => {
override(current_user, "email", me.email);
// should return null for non-stream messages containing no
// `with` operator
let raw_terms = [{operator: "channel", operand: foo_stream_id.toString()}];
let message = {type: "private"};
let result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.strictEqual(result, null);
// should adjust terms to contain `dm` for non-stream messages
// if it contains `with` operator
message = {
type: "private",
id: 2,
display_recipient: [
{id: 3, email: "user3@zulip.com"},
{id: me.user_id, email: me.email},
],
};
raw_terms = [
{operator: "channel", operand: foo_stream_id.toString()},
{operator: "with", operand: `${message.id}`},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepEqual(result, [
{operator: "dm", operand: [3], negated: false},
{operator: "with", operand: "2"},
]);
message = {
type: "private",
id: 2,
display_recipient: [{id: me.user_id, email: me.email}],
};
raw_terms = [
{operator: "channel", operand: "Foo"},
{operator: "with", operand: `${message.id}`},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepEqual(result, [
{operator: "dm", operand: [me.user_id], negated: false},
{operator: "with", operand: "2"},
]);
// should return null if no terms are changed
raw_terms = [{operator: "channel", operand: general_sub.stream_id.toString()}];
message = {
type: "stream",
stream_id: general_sub.stream_id,
display_recipient: "general",
topic: "discussion",
};
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.strictEqual(result, null);
// should adjust channel term to match message's display_recipient
raw_terms = [{operator: "channel", operand: "999"}];
message = {
type: "stream",
stream_id: general_sub.stream_id,
display_recipient: "general",
topic: "discussion",
};
let expected = [
{negated: false, operator: "channel", operand: general_sub.stream_id.toString()},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should adjust topic term to match message's topic
raw_terms = [{operator: "topic", operand: "random"}];
message = {
type: "stream",
stream_id: general_sub.stream_id,
display_recipient: "general",
topic: "discussion",
};
expected = [{negated: false, operator: "topic", operand: "discussion"}];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should adjust both channel and topic terms when both are different
raw_terms = [
{operator: "channel", operand: "999"},
{operator: "topic", operand: "random"},
];
message = {
type: "stream",
stream_id: general_sub.stream_id,
display_recipient: "general",
topic: "discussion",
};
expected = [
{negated: false, operator: "channel", operand: general_sub.stream_id.toString()},
{negated: false, operator: "topic", operand: "discussion"},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should not adjust terms that are not channel or topic
raw_terms = [
{operator: "channel", operand: "999"},
{operator: "topic", operand: "random"},
{operator: "sender", operand: alice.user_id},
];
message = {
type: "stream",
stream_id: general_sub.stream_id,
display_recipient: "general",
topic: "discussion",
};
expected = [
{negated: false, operator: "channel", operand: general_sub.stream_id.toString()},
{negated: false, operator: "topic", operand: "discussion"},
{negated: false, operator: "sender", operand: alice.user_id},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
});
run_test("can_newly_match_moved_messages", () => {
// Matches channel by ID
const stream_id_str = general_sub.stream_id.toString();
let filter = new Filter([{operator: "channel", operand: stream_id_str}]);
assert.deepEqual(filter.can_newly_match_moved_messages(stream_id_str, "test"), true);
assert.deepEqual(filter.can_newly_match_moved_messages("99999", "test"), false);
// Matches topic
filter = new Filter([{operator: "topic", operand: "Test topic"}]);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "Test topic"), true);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "test topic"), true);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "random topic"), false);
// Matches common narrows
filter = new Filter([{operator: "is", operand: "followed"}]);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "test"), true);
filter = new Filter([{operator: "is", operand: "starred"}]);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "test"), false);
filter = new Filter([
{negated: true, operator: "channel", operand: general_sub.stream_id.toString()},
]);
assert.deepEqual(filter.can_newly_match_moved_messages("something-else", "test"), true);
filter = new Filter([{negated: true, operator: "is", operand: "followed"}]);
assert.deepEqual(filter.can_newly_match_moved_messages("general", "test"), true);
});
run_test("terms_for_server_query", () => {
// Returns the raw terms unchanged (channel operand stays a
// string), only dropping the client-side-only `date` operator.
const filter = new Filter([
{operator: "channel", operand: "1"},
{operator: "date", operand: "2025-03-01"},
{operator: "topic", operand: "bar"},
]);
assert.deepEqual(filter.terms_for_server_query(), [
{operator: "channel", operand: "1", negated: false},
{operator: "topic", operand: "bar", negated: false},
]);
});
run_test("get_stringified_narrow_for_server_query", () => {
const filter = new Filter([
{operator: "channel", operand: "1"},
{operator: "topic", operand: "bar"},
]);
const narrow = filter.get_stringified_narrow_for_server_query();
assert.equal(
narrow,
'[{"operator":"channel","operand":1,"negated":false},{"operator":"topic","operand":"bar","negated":false}]',
);
// Excludes date term if it is present in the filter.
const filter2 = new Filter([
{operator: "channel", operand: "1"},
{operator: "date", operand: "2025-03-01"},
{operator: "topic", operand: "bar"},
]);
const narrow2 = filter2.get_stringified_narrow_for_server_query();
assert.equal(
narrow2,
'[{"operator":"channel","operand":1,"negated":false},{"operator":"topic","operand":"bar","negated":false}]',
);
});