/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/tests/filter.test.cjs
3 506 строк
134 KB
Anders Kaseorg
eslint: Fix unicorn/prefer-unicode-code-point-escapes.
29 июл 2026, 02:35
29 июл 2026, 02:35
63d02db
Код
Авторство
О чём код?
"use strict"; const assert = require("node:assert/strict"); const {parseOneAddress} = require("email-addresses"); const {make_realm} = require("./lib/example_realm.cjs"); const {make_stream} = require("./lib/example_stream.cjs"); const {make_user, Role} = require("./lib/example_user.cjs"); const {mock_esm, with_overrides, zrequire, clock} = require("./lib/namespace.cjs"); const {run_test} = require("./lib/test.cjs"); const blueslip = require("./lib/zblueslip.cjs"); const {$} = require("./lib/zjquery.cjs"); const {page_params} = require("./lib/zpage_params.cjs"); const message_store = zrequire("message_store"); const user_topics = mock_esm("../src/user_topics"); const resolved_topic = zrequire("resolved_topic"); const stream_data = zrequire("stream_data"); const people = zrequire("people"); const {Filter} = zrequire("filter"); const filter_util = zrequire("filter_util"); const {set_current_user, set_realm} = zrequire("state_data"); const {initialize_user_settings} = zrequire("user_settings"); const muted_users = zrequire("muted_users"); const state_data = zrequire("state_data"); const realm = make_realm(); set_realm(realm); const current_user = {}; set_current_user(current_user); initialize_user_settings({user_settings: {}}); const stream_message = "stream"; const direct_message = "private"; const me = make_user({ email: "me@example.com", user_id: 30, full_name: "Me Myself", }); const joe = make_user({ email: "joe@example.com", user_id: 31, full_name: "joe", }); const steve = make_user({ email: "STEVE@foo.com", user_id: 32, full_name: "steve", }); const alice = make_user({ email: "alice@example.com", user_id: 33, full_name: "alice", role: Role.GUEST, }); const jeff = make_user({ email: "jeff@foo.com", user_id: 34, full_name: "jeff", }); const annie = make_user({ email: "annie@foo.com", user_id: 35, full_name: "annie", role: Role.GUEST, }); // Multi-word names, to exercise resolving typed full names to user ids. const karl = make_user({ email: "karl@foo.com", user_id: 36, full_name: "Karl Stolley", }); const shubham = make_user({ email: "shubham@foo.com", user_id: 37, full_name: "Shubham Padia", }); // Two users sharing a full name, to exercise duplicate-name handling. const sam_one = make_user({ email: "sam1@foo.com", user_id: 38, full_name: "Sam Park", }); const sam_two = make_user({ email: "sam2@foo.com", user_id: 39, full_name: "Sam Park", }); // Add users to `valid_user_ids`. const source = "server_events"; people.add_active_user(me, source); people.add_active_user(joe, source); people.add_active_user(steve, source); people.add_active_user(alice, source); people.add_active_user(jeff, source); people.add_active_user(annie, source); people.add_active_user(karl, source); people.add_active_user(shubham, source); people.add_active_user(sam_one, source); people.add_active_user(sam_two, source); people.initialize_current_user(me.user_id); state_data.set_current_user(me); muted_users.add_muted_user(jeff.user_id); muted_users.add_muted_user(annie.user_id); function assert_same_terms(result, terms) { // If negated flag is undefined, we explicitly // set it to false. terms = terms.map(({negated = false, operator, operand}) => ({negated, operator, operand})); assert.deepEqual(result, terms); } function get_predicate(raw_terms) { const terms = raw_terms.map((op) => ({ operator: op[0], operand: op[1], })); return new Filter(terms).predicate(); } function make_sub(name, stream_id) { const sub = make_stream({ name, stream_id, }); stream_data.add_sub_for_tests(sub); } let _stream_id = 0; function new_stream_id() { _stream_id += 1; return _stream_id; } const foo_stream_id = new_stream_id(); const foo_sub = make_stream({ name: "Foo", stream_id: foo_stream_id, }); const general_sub = make_stream({ name: "general", stream_id: new_stream_id(), }); stream_data.add_sub_for_tests(general_sub); const invalid_sub_id = new_stream_id(); function test(label, f) { run_test(label, (helpers) => { stream_data.clear_subscriptions(); f(helpers); }); } test("basics", () => { let terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "channel", operand: invalid_sub_id.toString(), negated: true}, // excluded {operator: "topic", operand: "bar"}, ]; let filter = new Filter(terms); assert_same_terms(filter.terms(), terms); let channel_terms = filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: foo_stream_id.toString(), negated: false}, ]); assert.ok(filter.has_operator("channel")); assert.ok(!filter.has_operator("search")); assert.ok(filter.has_operand("channel", foo_stream_id.toString())); assert.ok(!filter.has_operand("channel", invalid_sub_id.toString())); assert.ok(!filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(filter.can_bucket_by("channel")); assert.ok(filter.can_bucket_by("channel", "topic")); assert.ok(!filter.has_exactly_channel_topic_operators()); // "stream" was renamed to "channel" terms = [{operator: "stream", operand: foo_stream_id.toString()}]; filter = new Filter(terms); assert.ok(filter.has_operator("channel")); channel_terms = filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: foo_stream_id.toString(), negated: false}, ]); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "channel", operand: invalid_sub_id.toString(), negated: true}, // excluded {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); channel_terms = filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: foo_stream_id.toString(), negated: false}, ]); assert.ok(filter.has_operator("channel")); assert.ok(!filter.has_operator("search")); assert.ok(filter.has_operand("channel", foo_stream_id.toString())); assert.ok(!filter.has_operand("channel", invalid_sub_id.toString())); assert.ok(!filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, {operator: "search", operand: "pizza"}, ]; filter = new Filter(terms); assert.ok(filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.allow_use_first_unread_when_narrowing()); assert.ok(!filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.can_bucket_by("channel")); assert.ok(filter.can_bucket_by("channel", "topic")); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, {operator: "near", operand: "17"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.can_bucket_by("channel")); assert.ok(filter.can_bucket_by("channel", "topic")); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.has_exactly_channel_topic_operators()); // If our only channel operator is negated, then for all intents and purposes, // we don't consider ourselves to have a channel operator, because we don't // want to have the channel in the tab bar or unsubscribe messaging, etc. terms = [{operator: "channel", operand: invalid_sub_id.toString(), negated: true}]; filter = new Filter(terms); assert.ok(filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("channel")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); // Negated searches are just like positive searches for our purposes, since // the search logic happens on the backend and we need to have can_apply_locally() // be false, and we want "Search results" in the tab bar. terms = [{operator: "search", operand: "stop_word", negated: true}]; filter = new Filter(terms); assert.ok(filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.has_operator("search")); assert.ok(!filter.can_apply_locally()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.has_exactly_channel_topic_operators()); // Similar logic applies to negated "has" searches. terms = [{operator: "has", operand: "images", negated: true}]; filter = new Filter(terms); assert.ok(filter.has_operator("has")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.can_apply_locally(true)); assert.ok(!filter.includes_full_stream_history()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "channels", operand: "public", negated: true}]; filter = new Filter(terms); assert.ok(filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("channels")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.has_negated_operand("channels", "public")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "channels", operand: "public"}]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.has_operator("channels")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_negated_operand("channels", "public")); assert.ok(filter.can_apply_locally()); assert.ok(filter.includes_full_stream_history()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "channels", operand: "archived", negated: true}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("channels")); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.has_negated_operand("channels", "archived")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "channels", operand: "archived"}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.has_operator("channels")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_negated_operand("channels", "archived")); assert.ok(filter.can_apply_locally()); assert.ok(filter.includes_full_stream_history()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.has_exactly_channel_topic_operators()); // "streams" was renamed to "channels" terms = [{operator: "streams", operand: "public"}]; filter = new Filter(terms); assert.ok(filter.has_operator("channels")); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.includes_full_stream_history()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "is", operand: "dm"}]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "is", operand: "dm", negated: true}]; filter = new Filter(terms); assert.ok(filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.has_exactly_channel_topic_operators()); // "is:private" was renamed to "is:dm" terms = [{operator: "is", operand: "private"}]; filter = new Filter(terms); assert.ok(filter.has_operand("is", "dm")); assert.ok(!filter.has_operand("is", "private")); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "is", operand: "mentioned"}]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.can_show_next_unread_topic_conversation_button()); assert.ok(!filter.can_show_next_unread_dm_conversation_button()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "is", operand: "starred"}]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "dm", operand: [joe.user_id]}]; filter = new Filter(terms); assert.ok(filter.is_search_for_specific_group_or_user()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "dm", operand: [joe.user_id]}, {operator: "near", operand: "17"}, ]; filter = new Filter(terms); assert.ok(filter.is_search_for_specific_group_or_user()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "dm", operand: [joe.user_id, steve.user_id]}]; filter = new Filter(terms); assert.ok(filter.is_search_for_specific_group_or_user()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "dm", operand: [joe.user_id, steve.user_id]}, {operator: "with", operand: "12"}, ]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); // "pm-with" was renamed to "dm" terms = [{operator: "pm-with", operand: [joe.user_id]}]; filter = new Filter(terms); assert.ok(filter.has_operator("dm")); assert.ok(!filter.has_operator(" pm-with")); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "dm-including", operand: [joe.user_id]}]; filter = new Filter(terms); assert.ok(filter.is_search_for_specific_group_or_user()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(filter.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); // "group-pm-with" was replaced with "dm-including" terms = [{operator: "group-pm-with", operand: [joe.user_id]}]; filter = new Filter(terms); assert.ok(filter.has_operator("dm-including")); assert.ok(!filter.has_operator("group-pm-with")); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [{operator: "is", operand: "resolved"}]; filter = new Filter(terms); assert.ok(filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); // Highly complex query to exercise // filter.contains_no_partial_conversations loop. terms = [ {operator: "is", operand: "resolved", negated: true}, {operator: "is", operand: "followed", negated: true}, {operator: "is", operand: "dm", negated: true}, {operator: "channel", operand: "channel_name", negated: true}, {operator: "channels", operand: "web-public", negated: true}, {operator: "channels", operand: "public"}, {operator: "topic", operand: "patience", negated: true}, {operator: "in", operand: "all"}, ]; filter = new Filter(terms); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); // This next check verifies what is probably a bug; see the // comment in the can_apply_locally implementation. assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, {operator: "with", operand: "17"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(filter.can_bucket_by("channel", "topic", "with")); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); // "stream" was renamed to "channel" terms = [ {operator: "stream", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); assert.ok(!filter.is_conversation_view_with_near()); assert.ok(!filter.is_channel_view()); assert.ok(filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: "channel_name"}, {operator: "channels", operand: "public"}, {operator: "topic", operand: "patience"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.contains_no_partial_conversations()); assert.ok(!filter.may_have_incomplete_message_history()); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.allow_use_first_unread_when_narrowing()); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.may_contain_multiple_conversations()); assert.ok(!filter.is_channel_view()); assert.ok(!filter.has_exactly_channel_topic_operators()); terms = [ {operator: "channel", operand: "foo", negated: false}, {operator: "topic", operand: "bar", negated: false}, ]; filter = new Filter(terms); assert.equal(filter.has_exactly_channel_topic_operators(), true); filter.adjust_with_operand_to_message(12); assert.deepEqual(filter.terms(), [...terms, {operator: "with", operand: "12"}]); assert.equal(filter.has_exactly_channel_topic_operators(), false); terms = [{operator: "channel", operand: "foo", negated: false}]; filter = new Filter(terms); assert.ok(filter.is_channel_view()); terms = [{operator: "mentions", operand: joe.user_id}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.contains_no_partial_conversations()); assert.ok(!filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); assert.ok(!filter.is_channel_view()); // Throw error on invalid operator. assert.throws(() => get_predicate([["bogus", "33"]]), { name: "$ZodError", }); }); function assert_not_mark_read_with_has_operands(additional_terms_to_test) { additional_terms_to_test ||= []; let has_link_term = [{operator: "has", operand: "link"}]; let filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "link", negated: true}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "image"}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "image", negated: true}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "attachment", negated: true}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "attachment"}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); has_link_term = [{operator: "has", operand: "reaction"}]; filter = new Filter([...additional_terms_to_test, ...has_link_term]); assert.ok(!filter.can_mark_messages_read()); } function assert_not_mark_read_with_is_operands(additional_terms_to_test) { additional_terms_to_test ||= []; let is_operator = [{operator: "is", operand: "starred"}]; let filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "starred", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "mentioned"}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "mentioned", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "alerted"}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "alerted", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "unread"}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "unread", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "resolved"}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); if (additional_terms_to_test.length === 0) { assert.ok(filter.can_mark_messages_read()); } else { assert.ok(!filter.can_mark_messages_read()); } is_operator = [{operator: "is", operand: "resolved", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); is_operator = [{operator: "is", operand: "followed", negated: true}]; filter = new Filter([...additional_terms_to_test, ...is_operator]); assert.ok(!filter.can_mark_messages_read()); } function assert_not_mark_read_when_searching(additional_terms_to_test) { additional_terms_to_test ||= []; let search_op = [{operator: "search", operand: "keyword"}]; let filter = new Filter([...additional_terms_to_test, ...search_op]); assert.ok(!filter.can_mark_messages_read()); search_op = [{operator: "search", operand: "keyword", negated: true}]; filter = new Filter([...additional_terms_to_test, ...search_op]); assert.ok(!filter.can_mark_messages_read()); } test("can_mark_messages_read", () => { assert_not_mark_read_with_has_operands(); assert_not_mark_read_with_is_operands(); assert_not_mark_read_when_searching(); const channel_term = [{operator: "channel", operand: foo_stream_id.toString()}]; let filter = new Filter(channel_term); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_has_operands(channel_term); assert_not_mark_read_with_is_operands(channel_term); assert_not_mark_read_when_searching(channel_term); const channel_negated_operator = [ {operator: "channel", operand: foo_stream_id.toString(), negated: true}, ]; filter = new Filter(channel_negated_operator); assert.ok(!filter.can_mark_messages_read()); const channel_topic_terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(channel_topic_terms); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_has_operands(channel_topic_terms); assert_not_mark_read_with_is_operands(channel_topic_terms); assert_not_mark_read_when_searching(channel_topic_terms); const channel_negated_topic_terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "bar", negated: true}, ]; filter = new Filter(channel_negated_topic_terms); assert.ok(!filter.can_mark_messages_read()); const dm = [{operator: "dm", operand: [joe.user_id]}]; const dm_negated = [{operator: "dm", operand: [joe.user_id], negated: true}]; const dm_group = [{operator: "dm", operand: [joe.user_id, steve.user_id]}]; filter = new Filter(dm); assert.ok(filter.can_mark_messages_read()); filter = new Filter(dm_negated); assert.ok(!filter.can_mark_messages_read()); filter = new Filter(dm_group); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_is_operands(dm_group); assert_not_mark_read_with_is_operands(dm); assert_not_mark_read_with_has_operands(dm_group); assert_not_mark_read_with_has_operands(dm); assert_not_mark_read_when_searching(dm_group); assert_not_mark_read_when_searching(dm); const is_dm = [{operator: "is", operand: "dm"}]; filter = new Filter(is_dm); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_is_operands(is_dm); assert_not_mark_read_with_has_operands(is_dm); assert_not_mark_read_when_searching(is_dm); const in_all = [{operator: "in", operand: "all"}]; filter = new Filter(in_all); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_is_operands(in_all); assert_not_mark_read_with_has_operands(in_all); assert_not_mark_read_when_searching(in_all); const in_home = [{operator: "in", operand: "home"}]; const in_home_negated = [{operator: "in", operand: "home", negated: true}]; filter = new Filter(in_home); assert.ok(filter.can_mark_messages_read()); assert_not_mark_read_with_is_operands(in_home); assert_not_mark_read_with_has_operands(in_home); assert_not_mark_read_when_searching(in_home); filter = new Filter(in_home_negated); assert.ok(!filter.can_mark_messages_read()); const is_muted = [{operator: "is", operand: "muted"}]; const is_muted_negated = [{operator: "is", operand: "muted", negated: true}]; filter = new Filter(is_muted); assert.ok(!filter.can_mark_messages_read()); assert_not_mark_read_with_is_operands(is_muted); assert_not_mark_read_with_has_operands(is_muted); assert_not_mark_read_when_searching(is_muted); filter = new Filter(is_muted_negated); assert.ok(filter.can_mark_messages_read()); // Do not mark messages as read when in an unsupported 'in:*' filter. const in_random = [{operator: "in", operand: "xxxxxxxxx"}]; const in_random_negated = [{operator: "in", operand: "xxxxxxxxx", negated: true}]; filter = new Filter(in_random); assert.ok(!filter.can_mark_messages_read()); filter = new Filter(in_random_negated); assert.ok(!filter.can_mark_messages_read()); // test caching of term types // init and stub filter = new Filter(dm); filter.stub = filter.calc_can_mark_messages_read; filter.calc_can_mark_messages_read = function () { this.calc_can_mark_messages_read_called = true; return this.stub(); }; // uncached trial filter.calc_can_mark_messages_read_called = false; assert.ok(filter.can_mark_messages_read()); assert.ok(filter.calc_can_mark_messages_read_called); // cached trial filter.calc_can_mark_messages_read_called = false; assert.ok(filter.can_mark_messages_read()); assert.ok(!filter.calc_can_mark_messages_read_called); }); test("show_first_unread", () => { let terms = [{operator: "is", operand: "any"}]; let filter = new Filter(terms); assert.ok(filter.allow_use_first_unread_when_narrowing()); terms = [{operator: "search", operand: "query to search"}]; filter = new Filter(terms); assert.ok(!filter.allow_use_first_unread_when_narrowing()); filter = new Filter([]); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.allow_use_first_unread_when_narrowing()); terms = [{operator: "is", operand: "starred"}]; filter = new Filter(terms); assert.ok(!filter.allow_use_first_unread_when_narrowing()); // Side case terms = [{operator: "is", operand: "any"}]; filter = new Filter(terms); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.allow_use_first_unread_when_narrowing()); }); test("filter_with_new_params_topic", () => { const terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "old topic"}, ]; const filter = new Filter(terms); assert.ok(filter.has_topic(foo_stream_id.toString(), "old topic")); assert.ok(!filter.has_topic(invalid_sub_id.toString(), "old topic")); assert.ok(!filter.has_topic(foo_stream_id.toString(), "wrong")); const new_filter = filter.filter_with_new_params({ operator: "topic", operand: "new topic", }); const channel_terms = new_filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: foo_stream_id.toString(), negated: false}, ]); const topic_terms = new_filter.terms_with_operator("topic"); assert.deepEqual(topic_terms, [{operator: "topic", operand: "new topic", negated: false}]); }); test("filter_with_new_params_channel", () => { const terms = [ {operator: "channel", operand: foo_stream_id.toString()}, {operator: "topic", operand: "old topic"}, ]; const filter = new Filter(terms); assert.ok(filter.has_topic(foo_stream_id.toString(), "old topic")); assert.ok(!filter.has_topic(invalid_sub_id.toString(), "old topic")); assert.ok(!filter.has_topic(foo_stream_id.toString(), "wrong")); const new_filter = filter.filter_with_new_params({ operator: "channel", operand: "new channel", }); const channel_terms = new_filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: "new channel", negated: false}, ]); const topic_terms = new_filter.terms_with_operator("topic"); assert.deepEqual(topic_terms, [{operator: "topic", operand: "old topic", negated: false}]); }); test("new_style_terms", () => { const term = { operator: "channel", operand: foo_stream_id.toString(), }; const terms = [term]; const filter = new Filter(terms); const channel_terms = filter.terms_with_operator("channel"); assert.deepEqual(channel_terms, [ {operator: "channel", operand: foo_stream_id.toString(), negated: false}, ]); assert.ok(filter.can_bucket_by("channel")); }); test("public_terms", ({override}) => { stream_data.clear_subscriptions(); const some_channel_id = new_stream_id(); const default_channel_id = new_stream_id(); make_sub("default", default_channel_id); let terms = [ {operator: "channel", operand: some_channel_id.toString()}, {operator: "in", operand: "all"}, {operator: "topic", operand: "bar"}, {operator: "date", operand: "2025-04-01"}, ]; let filter = new Filter(terms); const expected_terms = [ {operator: "channel", operand: some_channel_id.toString()}, {operator: "in", operand: "all"}, {operator: "topic", operand: "bar"}, ]; override(page_params, "narrow_stream", undefined); assert_same_terms(filter.public_terms(), expected_terms); assert.ok(filter.can_bucket_by("channel")); terms = [{operator: "channel", operand: default_channel_id.toString()}]; filter = new Filter(terms); override(page_params, "narrow_stream", "default"); assert_same_terms(filter.public_terms(), []); }); test("redundancies", () => { let terms; let filter; terms = [ {operator: "dm", operand: [joe.user_id]}, {operator: "is", operand: "dm"}, ]; filter = new Filter(terms); assert.ok(filter.can_bucket_by("dm")); terms = [ {operator: "dm", operand: [joe.user_id], negated: true}, {operator: "is", operand: "dm"}, ]; filter = new Filter(terms); assert.ok(filter.can_bucket_by("is-dm", "not-dm")); }); test("canonicalization", () => { assert.equal(filter_util.canonicalize_operator("is"), "is"); assert.equal(filter_util.canonicalize_operator("stream"), "channel"); assert.equal(filter_util.canonicalize_operator("subject"), "topic"); assert.equal(filter_util.canonicalize_operator("from"), "sender"); let term; term = Filter.canonicalize_term({operator: "stream", operand: "Denmark"}); assert.equal(term.operator, "channel"); assert.equal(term.operand, "Denmark"); term = Filter.canonicalize_term({operator: "channel", operand: "Denmark"}); assert.equal(term.operator, "channel"); assert.equal(term.operand, "Denmark"); term = Filter.convert_suggestion_to_term({operator: "sender", operand: "me"}); assert.equal(term.operator, "sender"); assert.equal(term.operand, me.user_id); // mentions:me redirects to is:mentioned term = Filter.convert_suggestion_to_term({operator: "mentions", operand: "me"}); assert.equal(term.operator, "is"); assert.equal(term.operand, "mentioned"); // "pm-with" was renamed to "dm" term = Filter.convert_suggestion_to_term({operator: "pm-with", operand: "me"}); assert.equal(term.operator, "dm"); assert.deepEqual(term.operand, [me.user_id]); // "group-pm-with" was replaced with "dm-including" term = Filter.canonicalize_term({operator: "group-pm-with", operand: [joe.user_id]}); assert.equal(term.operator, "dm-including"); assert.deepEqual(term.operand, [joe.user_id]); term = Filter.canonicalize_term({operator: "search", operand: "foo"}); assert.equal(term.operator, "search"); assert.equal(term.operand, "foo"); term = Filter.canonicalize_term({operator: "search", operand: "fOO"}); assert.equal(term.operator, "search"); assert.equal(term.operand, "fOO"); term = Filter.canonicalize_term({operator: "search", operand: "123"}); assert.equal(term.operator, "search"); assert.equal(term.operand, "123"); term = Filter.canonicalize_term({operator: "search", operand: "abc “xyz”"}); assert.equal(term.operator, "search"); assert.equal(term.operand, 'abc "xyz"'); term = Filter.canonicalize_term({operator: "date", operand: "2022-06-10"}); assert.equal(term.operator, "date"); assert.equal(term.operand, "2022-06-10"); term = Filter.canonicalize_term({operator: "has", operand: "attachments"}); assert.equal(term.operator, "has"); assert.equal(term.operand, "attachment"); term = Filter.canonicalize_term({operator: "has", operand: "images"}); assert.equal(term.operator, "has"); assert.equal(term.operand, "image"); term = Filter.canonicalize_term({operator: "has", operand: "links"}); assert.equal(term.operator, "has"); assert.equal(term.operand, "link"); term = Filter.canonicalize_term({operator: "has", operand: "reactions"}); assert.equal(term.operator, "has"); assert.equal(term.operand, "reaction"); term = Filter.canonicalize_term({operator: "mentions", operand: joe.user_id}); assert.equal(term.operator, "mentions"); assert.equal(term.operand, joe.user_id); }); test("ensure_channel_topic_terms", () => { const channel_term = {operator: "channel", operand: `${general_sub.stream_id}`}; const topic_term = {operator: "topic", operand: "discussion"}; const message = { type: "stream", id: 12, stream_id: general_sub.stream_id, display_recipient: "general", topic: "discussion", }; const term_1 = Filter.ensure_channel_topic_terms( [{operator: "with", operand: message.id}], message, ); const term_2 = Filter.ensure_channel_topic_terms( [topic_term, {operator: "with", operand: message.id}], message, ); const term_3 = Filter.ensure_channel_topic_terms( [channel_term, {operator: "with", operand: message.id}], message, ); const term_4 = Filter.ensure_channel_topic_terms( [ {operator: "dm", operand: [joe.user_id]}, {operator: "with", operand: message.id}, ], message, ); const term_5 = Filter.ensure_channel_topic_terms( [{operator: "with", operand: message.id}], message, ); const terms = [term_1, term_2, term_3, term_4, term_5]; for (const term of terms) { assert.deepEqual(term, [channel_term, topic_term, {operator: "with", operand: 12}]); } }); test("predicate_basics", ({override}) => { // Predicates are functions that accept a message object with the message // attributes (not content), and return true if the message belongs in a // given narrow. If the narrow parameters include a search, the predicate // passes through all messages. // // To keep these tests simple, we only pass objects with a few relevant attributes // rather than full-fledged message objects. stream_data.add_sub_for_tests(foo_sub); let predicate = get_predicate([ ["channel", foo_stream_id.toString()], ["topic", "Bar"], ]); assert.ok(predicate({type: stream_message, stream_id: foo_stream_id, topic: "bar"})); assert.ok(!predicate({type: stream_message, stream_id: foo_stream_id, topic: "whatever"})); // 9999999 doesn't exist, testing no match assert.ok(!predicate({type: stream_message, stream_id: 9999999})); assert.ok(!predicate({type: direct_message})); // Three or more terms: channel + topic + is:starred. predicate = get_predicate([ ["channel", foo_stream_id.toString()], ["topic", "Bar"], ["is", "starred"], ]); assert.ok( predicate({type: stream_message, stream_id: foo_stream_id, topic: "bar", starred: true}), ); assert.ok( !predicate({type: stream_message, stream_id: foo_stream_id, topic: "bar", starred: false}), ); assert.ok( !predicate({ type: stream_message, stream_id: foo_stream_id, topic: "whatever", starred: true, }), ); assert.ok(!predicate({type: direct_message, starred: true})); // For old channels that we are no longer subscribed to, we may not have // a subscription, but these should still match by channel name. const old_sub_id = new_stream_id(); const private_sub_id = new_stream_id(); const web_public_sub_id = new_stream_id(); const archived_sub_id = new_stream_id(); const old_sub = { name: "old-subscription", stream_id: old_sub_id, subscribed: false, invite_only: false, is_web_public: false, }; const private_sub = { name: "private-subscription", stream_id: private_sub_id, subscribed: true, invite_only: true, is_web_public: false, }; const web_public_sub = { name: "web-public-subscription", stream_id: web_public_sub_id, subscribed: false, invite_only: false, is_web_public: true, }; const archived_sub = { name: "archived-subscription", stream_id: archived_sub_id, subscribed: true, invite_only: false, is_web_public: true, is_archived: true, }; stream_data.add_sub_for_tests(old_sub); stream_data.add_sub_for_tests(private_sub); stream_data.add_sub_for_tests(web_public_sub); stream_data.add_sub_for_tests(archived_sub); predicate = get_predicate([ ["channel", old_sub_id.toString()], ["topic", "Bar"], ]); assert.ok(predicate({type: stream_message, stream_id: old_sub.stream_id, topic: "bar"})); // 99999 doesn't exist, testing no match assert.ok(!predicate({type: stream_message, stream_id: invalid_sub_id, topic: "whatever"})); predicate = get_predicate([["search", "emoji"]]); assert.ok(predicate({})); predicate = get_predicate([["topic", "Bar"]]); assert.ok(!predicate({type: direct_message})); predicate = get_predicate([["is", "dm"]]); assert.ok(predicate({type: direct_message})); assert.ok(!predicate({type: stream_message})); predicate = get_predicate([["channels", "public"]]); assert.ok(predicate({type: stream_message, stream_id: old_sub_id})); assert.ok(!predicate({type: stream_message, stream_id: private_sub_id})); assert.ok(predicate({type: stream_message, stream_id: web_public_sub_id})); predicate = get_predicate([["channels", "archived"]]); assert.ok(predicate({type: stream_message, stream_id: archived_sub_id})); predicate = get_predicate([["channels", "web-public"]]); assert.ok(predicate({type: stream_message, stream_id: archived_sub_id})); assert.ok(predicate({type: stream_message, stream_id: web_public_sub_id})); assert.ok(!predicate({type: stream_message, stream_id: old_sub_id})); predicate = get_predicate([["channels", "bogus"]]); assert.ok(!predicate({type: stream_message, stream_id: old_sub_id})); predicate = get_predicate([["is", "starred"]]); assert.ok(predicate({starred: true})); assert.ok(!predicate({starred: false})); predicate = get_predicate([["is", "unread"]]); assert.ok(predicate({unread: true})); assert.ok(!predicate({unread: false})); predicate = get_predicate([["is", "alerted"]]); assert.ok(predicate({alerted: true})); assert.ok(!predicate({alerted: false})); assert.ok(!predicate({})); predicate = get_predicate([["is", "mentioned"]]); assert.ok(predicate({mentioned: true})); assert.ok(!predicate({mentioned: false})); predicate = get_predicate([["in", "all"]]); assert.ok(predicate({})); predicate = get_predicate([["is", "resolved"]]); const resolved_topic_name = resolved_topic.resolve_name("foo"); assert.ok(predicate({type: stream_message, topic: resolved_topic_name})); assert.ok(!predicate({topic: resolved_topic_name})); assert.ok(!predicate({type: stream_message, topic: "foo"})); predicate = get_predicate([["is", "followed"]]); override(user_topics, "is_topic_followed", () => false); assert.ok(!predicate({type: "stream", topic: "foo", stream_id: 5})); override(user_topics, "is_topic_followed", () => true); assert.ok(predicate({type: "stream", topic: "foo", stream_id: 5})); const unknown_stream_id = new_stream_id(); override(user_topics, "is_topic_visible_in_home", () => false); predicate = get_predicate([["in", "home"]]); assert.ok(!predicate({stream_id: unknown_stream_id, stream: "unknown"})); assert.ok(predicate({type: direct_message})); // Muted topic is not part of in-home. with_overrides(({override}) => { override(user_topics, "is_topic_visible_in_home", () => false); assert.ok(!predicate({stream_id: foo_stream_id, topic: "bar"})); }); // Muted stream is not part of in-home. const muted_stream = { stream_id: 94924, name: "muted", is_muted: true, }; stream_data.add_sub_for_tests(muted_stream); assert.ok(!predicate({stream_id: muted_stream.stream_id, topic: "bar"})); // Muted stream but topic is unmuted or followed is part of in-home. with_overrides(({override}) => { override(user_topics, "is_topic_visible_in_home", () => true); assert.ok(predicate({stream_id: muted_stream.stream_id, topic: "bar"})); }); make_sub("kiosk", 1234); with_overrides(({override}) => { override(page_params, "narrow_stream", "kiosk"); assert.ok(predicate({stream_id: 1234})); }); override(user_topics, "is_topic_visible_in_home", () => false); predicate = get_predicate([["is", "muted"]]); assert.ok(predicate({stream_id: unknown_stream_id, stream: "unknown"})); assert.ok(!predicate({type: direct_message})); // Muted topic is a part of is-muted. with_overrides(({override}) => { override(user_topics, "is_topic_visible_in_home", () => false); assert.ok(predicate({stream_id: foo_stream_id, topic: "bar"})); }); // Muted stream is a part of is:muted. assert.ok(predicate({stream_id: muted_stream.stream_id, topic: "bar"})); // Muted stream but topic is unmuted or followed is not a part of is-muted. with_overrides(({override}) => { override(user_topics, "is_topic_visible_in_home", () => true); assert.ok(!predicate({stream_id: muted_stream.stream_id, topic: "bar"})); }); predicate = get_predicate([["near", "5"]]); assert.ok(predicate({})); predicate = get_predicate([["id", "5"]]); assert.ok(predicate({id: 5})); assert.ok(!predicate({id: 6})); predicate = get_predicate([ ["id", "5"], ["topic", "lunch"], ]); assert.ok(predicate({type: stream_message, id: 5, topic: "lunch"})); assert.ok(!predicate({type: stream_message, id: 5, topic: "dinner"})); predicate = get_predicate([["sender", joe.user_id]]); assert.ok(predicate({sender_id: joe.user_id})); assert.ok(!predicate({sender_email: steve.user_id})); predicate = get_predicate([["dm", [joe.user_id]]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}], }), ); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: steve.user_id}], }), ); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: 999999}], }), ); assert.ok(!predicate({type: stream_message})); predicate = get_predicate([["dm", [joe.user_id, steve.user_id]]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: steve.user_id}], }), ); // A 1:1 DM message should not match a group DM predicate. assert.ok( !predicate({ type: direct_message, display_recipient: [{id: joe.user_id}], }), ); // Make sure your own user id is ignored predicate = get_predicate([["dm", [joe.user_id, steve.user_id, me.user_id]]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: steve.user_id}], }), ); predicate = get_predicate([["dm", [1000]]]); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: joe.user_id}], }), ); predicate = get_predicate([["dm-including", [1000]]]); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: me.user_id}], }), ); predicate = get_predicate([["dm-including", [joe.user_id]]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: steve.user_id}, {id: me.user_id}], }), ); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: me.user_id}], }), ); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: steve.user_id}, {id: me.user_id}], }), ); assert.ok(!predicate({type: stream_message})); const inline_audio_msg = { content: '<p><audio controls preload="metadata" src="/user_uploads/randompath/test.mp3" title="zulip.mp3"></audio></p>', }; predicate = get_predicate([["mentions", joe.user_id]]); assert.ok(predicate({})); const inline_img_msg = { content: '<p><img alt="Screenshot" class="inline-image" data-original-content-type="image/png" data-original-dimensions="1488x1130" data-original-src="/user_uploads/randompath/test.png" src="/user_uploads/thumbnail/randompath/test.png/840x560.webp"></p>', }; 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: '<p><a href="/user_uploads/randompath/test.jpeg">test.jpeg</a></p><div class="message_inline_image"><a href="/user_uploads/randompath/test.jpeg" title="test.jpeg"><img src="/user_uploads/randompath/test.jpeg"></a></div>', }; const link_msg = { content: '<p><a href="http://chat.zulip.org">chat.zulip.org</a></p>', }; const non_img_attachment_msg = { content: '<p><a href="/user_uploads/randompath/attachment.ext">attachment.ext</a></p>', }; const no_has_filter_matching_msg = { content: "<p>Testing</p>", }; 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) { $(`<div>${msg.content}</div>`).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 = `<span class="decorated-channel-name-wrapper inline-decorated-channel-name"><span class="channel-privacy-type-icon"><i class="zulip-icon zulip-icon-hashtag" aria-hidden="true"></i></span><span class="decorated-channel-name">devel</span></span>`; const river_decorated = `<span class="decorated-channel-name-wrapper inline-decorated-channel-name"><span class="channel-privacy-type-icon"><i class="zulip-icon zulip-icon-hashtag" aria-hidden="true"></i></span><span class="decorated-channel-name">river</span></span>`; 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} > <span class="empty-topic-display">translated: general chat</span>`; assert.equal(Filter.search_description_as_html(narrow, false), string); narrow = [ {operator: "topic", operand: ""}, {operator: "is", operand: "starred"}, ]; string = 'topic <span class="empty-topic-display">translated: general chat</span>, starred messages'; assert.equal(Filter.search_description_as_html(narrow, false), string); narrow = [{operator: "topic", operand: ""}]; string = `topic <span class="empty-topic-display">translated: general chat</span>`; 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}]', ); });