"use strict"; const {strict: assert} = require("assert"); const {parseOneAddress} = require("email-addresses"); const {mock_esm, with_overrides, zrequire} = require("./lib/namespace"); const {run_test} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {page_params, realm} = require("./lib/zpage_params"); const message_store = mock_esm("../src/message_store"); const resolved_topic = zrequire("../shared/src/resolved_topic"); const stream_data = zrequire("stream_data"); const people = zrequire("people"); const {Filter} = zrequire("../src/filter"); const stream_message = "stream"; const direct_message = "private"; const me = { email: "me@example.com", user_id: 30, full_name: "Me Myself", }; const joe = { email: "joe@example.com", user_id: 31, full_name: "joe", }; const steve = { email: "STEVE@foo.com", user_id: 32, full_name: "steve", }; const alice = { email: "alice@example.com", user_id: 33, full_name: "alice", is_guest: true, }; people.add_active_user(me); people.add_active_user(joe); people.add_active_user(steve); people.add_active_user(alice); people.initialize_current_user(me.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 = { name, stream_id, }; stream_data.add_sub(sub); } function test(label, f) { run_test(label, (helpers) => { stream_data.clear_subscriptions(); f(helpers); }); } test("basics", () => { let terms = [ {operator: "channel", operand: "foo"}, {operator: "channel", operand: "exclude_me", negated: true}, {operator: "topic", operand: "bar"}, ]; let filter = new Filter(terms); assert_same_terms(filter.terms(), terms); assert.deepEqual(filter.operands("channel"), ["foo"]); assert.ok(filter.has_operator("channel")); assert.ok(!filter.has_operator("search")); assert.ok(filter.has_operand("channel", "foo")); assert.ok(!filter.has_operand("channel", "exclude_me")); assert.ok(!filter.has_operand("channel", "nada")); assert.ok(!filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); 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")); assert.ok(filter.can_bucket_by("channel", "topic")); // "stream" was renamed to "channel" terms = [{operator: "stream", operand: "foo"}]; assert.ok(filter.has_operator("channel")); assert.deepEqual(filter.operands("channel"), ["foo"]); assert.ok(filter.includes_full_stream_history()); assert.ok(filter.can_apply_locally()); terms = [ {operator: "channel", operand: "foo"}, {operator: "channel", operand: "exclude_me", negated: true}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); assert.deepEqual(filter.operands("channel"), ["foo"]); assert.ok(filter.has_operator("channel")); assert.ok(!filter.has_operator("search")); assert.ok(filter.has_operand("channel", "foo")); assert.ok(!filter.has_operand("channel", "exclude_me")); assert.ok(!filter.has_operand("channel", "nada")); assert.ok(!filter.is_keyword_search()); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); 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()); terms = [ {operator: "channel", operand: "foo"}, {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.supports_collapsing_recipients()); 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()); terms = [ {operator: "channel", operand: "foo"}, {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.supports_collapsing_recipients()); 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()); // 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: "exclude", negated: true}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.has_operator("channel")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); // 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.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.supports_collapsing_recipients()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); // 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.supports_collapsing_recipients()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); terms = [{operator: "channels", operand: "public", 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.supports_collapsing_recipients()); 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()); terms = [{operator: "channels", operand: "public"}]; 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.supports_collapsing_recipients()); 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()); // "streams" was renamed to "channels" terms = [{operator: "streams", operand: "public"}]; filter = new Filter(terms); assert.ok(filter.has_operator("channels")); assert.ok(filter.supports_collapsing_recipients()); assert.ok(filter.includes_full_stream_history()); terms = [{operator: "is", operand: "dm"}]; filter = new Filter(terms); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); // "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")); terms = [{operator: "is", operand: "mentioned"}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.supports_collapsing_recipients()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); terms = [{operator: "is", operand: "starred"}]; filter = new Filter(terms); assert.ok(!filter.contains_only_private_messages()); assert.ok(!filter.can_mark_messages_read()); assert.ok(!filter.supports_collapsing_recipients()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); terms = [{operator: "dm", operand: "joe@example.com"}]; filter = new Filter(terms); assert.ok(filter.is_non_huddle_pm()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); assert.ok(!filter.has_operator("search")); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); terms = [{operator: "dm", operand: "joe@example.com,jack@example.com"}]; filter = new Filter(terms); assert.ok(!filter.is_non_huddle_pm()); assert.ok(filter.contains_only_private_messages()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(filter.is_conversation_view()); // "pm-with" was renamed to "dm" terms = [{operator: "pm-with", operand: "joe@example.com"}]; filter = new Filter(terms); assert.ok(filter.has_operator("dm")); assert.ok(!filter.has_operator(" pm-with")); terms = [{operator: "dm-including", operand: "joe@example.com"}]; filter = new Filter(terms); assert.ok(!filter.is_non_huddle_pm()); assert.ok(filter.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); // "group-pm-with" was replaced with "dm-including" terms = [{operator: "group-pm-with", operand: "joe@example.com"}]; filter = new Filter(terms); assert.ok(filter.has_operator("dm-including")); assert.ok(!filter.has_operator("group-pm-with")); terms = [{operator: "is", operand: "resolved"}]; 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.supports_collapsing_recipients()); assert.ok(filter.can_apply_locally()); assert.ok(!filter.is_personal_filter()); assert.ok(!filter.is_conversation_view()); // Highly complex query to exercise // filter.supports_collapsing_recipients loop. terms = [ {operator: "is", operand: "resolved", 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.contains_only_private_messages()); assert.ok(!filter.has_operator("search")); assert.ok(!filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); // 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()); terms = [ {operator: "channel", operand: "foo"}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); 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()); // "stream" was renamed to "channel" terms = [ {operator: "stream", operand: "foo"}, {operator: "topic", operand: "bar"}, ]; filter = new Filter(terms); assert.ok(!filter.is_keyword_search()); assert.ok(filter.can_mark_messages_read()); assert.ok(filter.supports_collapsing_recipients()); 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()); }); function assert_not_mark_read_with_has_operands(additional_terms_to_test) { 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()); } function assert_not_mark_read_with_is_operands(additional_terms_to_test) { 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()); } function assert_not_mark_read_when_searching(additional_terms_to_test) { 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"}]; 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", negated: true}]; filter = new Filter(channel_negated_operator); assert.ok(!filter.can_mark_messages_read()); const channel_topic_terms = [ {operator: "channel", operand: "foo"}, {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"}, {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@example.com,"}]; const dm_negated = [{operator: "dm", operand: "joe@example.com,", negated: true}]; const dm_group = [{operator: "dm", operand: "joe@example.com,STEVE@foo.com"}]; 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()); // 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()); // 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"}, {operator: "topic", operand: "old topic"}, ]; const filter = new Filter(terms); assert.ok(filter.has_topic("foo", "old topic")); assert.ok(!filter.has_topic("wrong", "old topic")); assert.ok(!filter.has_topic("foo", "wrong")); const new_filter = filter.filter_with_new_params({ operator: "topic", operand: "new topic", }); assert.deepEqual(new_filter.operands("channel"), ["foo"]); assert.deepEqual(new_filter.operands("topic"), ["new topic"]); }); test("filter_with_new_params_channel", () => { const terms = [ {operator: "channel", operand: "foo"}, {operator: "topic", operand: "old topic"}, ]; const filter = new Filter(terms); assert.ok(filter.has_topic("foo", "old topic")); assert.ok(!filter.has_topic("wrong", "old topic")); assert.ok(!filter.has_topic("foo", "wrong")); const new_filter = filter.filter_with_new_params({ operator: "channel", operand: "new channel", }); assert.deepEqual(new_filter.operands("channel"), ["new channel"]); assert.deepEqual(new_filter.operands("topic"), ["old topic"]); }); test("new_style_terms", () => { const term = { operator: "channel", operand: "foo", }; const terms = [term]; const filter = new Filter(terms); assert.deepEqual(filter.operands("channel"), ["foo"]); assert.ok(filter.can_bucket_by("channel")); }); test("public_terms", ({override}) => { stream_data.clear_subscriptions(); let terms = [ {operator: "channel", operand: "some_channel"}, {operator: "in", operand: "all"}, {operator: "topic", operand: "bar"}, ]; let filter = new Filter(terms); const expected_terms = [ {operator: "channel", operand: "some_channel"}, {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"}]; 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@example.com,"}, {operator: "is", operand: "dm"}, ]; filter = new Filter(terms); assert.ok(filter.can_bucket_by("dm")); terms = [ {operator: "dm", operand: "joe@example.com,", 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.canonicalize_operator("Is"), "is"); assert.equal(Filter.canonicalize_operator("Stream"), "channel"); assert.equal(Filter.canonicalize_operator("Subject"), "topic"); assert.equal(Filter.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.canonicalize_term({operator: "sender", operand: "me"}); assert.equal(term.operator, "sender"); assert.equal(term.operand, "me@example.com"); // "pm-with" was renamed to "dm" term = Filter.canonicalize_term({operator: "pm-with", operand: "me"}); assert.equal(term.operator, "dm"); assert.equal(term.operand, "me@example.com"); // "group-pm-with" was replaced with "dm-including" term = Filter.canonicalize_term({operator: "group-pm-with", operand: "joe@example.com"}); assert.equal(term.operator, "dm-including"); assert.equal(term.operand, "joe@example.com"); 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: "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"); }); test("predicate_basics", () => { // 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. const stream_id = 42; make_sub("Foo", stream_id); let predicate = get_predicate([ ["channel", "Foo"], ["topic", "Bar"], ]); assert.ok(predicate({type: stream_message, stream_id, topic: "bar"})); assert.ok(!predicate({type: stream_message, 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})); // 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 = { name: "old-subscription", stream_id: 5, subscribed: false, }; stream_data.add_sub(old_sub); predicate = get_predicate([ ["channel", "old-subscription"], ["topic", "Bar"], ]); assert.ok(predicate({type: stream_message, stream_id: 5, topic: "bar"})); // 99999 doesn't exist, testing no match assert.ok(!predicate({type: stream_message, stream_id: 99999, 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({})); 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"})); const unknown_stream_id = 999; predicate = get_predicate([["in", "home"]]); assert.ok(!predicate({stream_id: unknown_stream_id, stream: "unknown"})); assert.ok(predicate({type: direct_message})); make_sub("kiosk", 1234); with_overrides(({override}) => { override(page_params, "narrow_stream", "kiosk"); assert.ok(predicate({stream_id: 1234})); }); 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@example.com"]]); assert.ok(predicate({sender_id: joe.user_id})); assert.ok(!predicate({sender_email: steve.user_id})); predicate = get_predicate([["dm", "Joe@example.com"]]); 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@example.com,steve@foo.com"]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: steve.user_id}], }), ); // Make sure your own email is ignored predicate = get_predicate([["dm", "Joe@example.com,steve@foo.com,me@example.com"]]); assert.ok( predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: steve.user_id}], }), ); predicate = get_predicate([["dm", "nobody@example.com"]]); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: joe.user_id}], }), ); predicate = get_predicate([["dm-including", "nobody@example.com"]]); assert.ok( !predicate({ type: direct_message, display_recipient: [{id: joe.user_id}, {id: me.user_id}], }), ); predicate = get_predicate([["dm-including", "Joe@example.com"]]); 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 img_msg = { content: '
', }; const link_msg = { content: '', }; const non_img_attachment_msg = { content: '', }; const no_has_filter_matching_msg = { content: "Testing
", }; predicate = get_predicate([["has", "non_valid_operand"]]); 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) { $(`