"use strict"; const assert = require("node:assert/strict"); const _ = require("lodash"); const MockDate = require("mockdate"); const {set_global, zrequire} = require("./lib/namespace.cjs"); const {run_test} = require("./lib/test.cjs"); const blueslip = zrequire("blueslip"); const {initialize_user_settings} = zrequire("user_settings"); set_global("document", {}); const util = zrequire("util"); initialize_user_settings({user_settings: {}}); run_test("CachedValue", () => { let x = 5; const cv = new util.CachedValue({ compute_value() { return x * 2; }, }); assert.equal(cv.get(), 10); x = 6; assert.equal(cv.get(), 10); cv.reset(); assert.equal(cv.get(), 12); }); run_test("extract_pm_recipients", () => { assert.equal(util.extract_pm_recipients("bob@foo.com, alice@foo.com").length, 2); assert.equal(util.extract_pm_recipients("bob@foo.com, ").length, 1); }); run_test("lower_bound", () => { const arr = [{x: 10}, {x: 20}, {x: 30}, {x: 40}, {x: 50}]; function compare(a, b) { return a.x < b; } assert.equal(util.lower_bound(arr, 5, compare), 0); assert.equal(util.lower_bound(arr, 10, compare), 0); assert.equal(util.lower_bound(arr, 15, compare), 1); assert.equal(util.lower_bound(arr, 50, compare), 4); assert.equal(util.lower_bound(arr, 55, compare), 5); }); run_test("lower_same", () => { assert.ok(util.lower_same("abc", "AbC")); assert.ok(!util.lower_same("abbc", "AbC")); blueslip.expect("error", "Cannot compare strings; at least one value is undefined"); util.lower_same("abc", undefined); }); run_test("same_recipient", () => { assert.ok( util.same_recipient( {type: "stream", stream_id: 101, topic: "Bar"}, {type: "stream", stream_id: 101, topic: "bar"}, ), ); assert.ok( !util.same_recipient( {type: "stream", stream_id: 101, topic: "Bar"}, {type: "stream", stream_id: 102, topic: "whatever"}, ), ); assert.ok( util.same_recipient( {type: "private", to_user_ids: "101,102"}, {type: "private", to_user_ids: "101,102"}, ), ); assert.ok( !util.same_recipient( {type: "private", to_user_ids: "101,102"}, {type: "private", to_user_ids: "103"}, ), ); assert.ok( !util.same_recipient({type: "stream", stream_id: 101, topic: "Bar"}, {type: "private"}), ); assert.ok(!util.same_recipient({type: "private", to_user_ids: undefined}, {type: "private"})); assert.ok(!util.same_recipient(undefined, {type: "private"})); assert.ok(!util.same_recipient(undefined, undefined)); }); run_test("robust_url_decode", ({override}) => { assert.equal(util.robust_url_decode("xxx%3Ayyy"), "xxx:yyy"); assert.equal(util.robust_url_decode("xxx%3"), "xxx"); override(global, "decodeURIComponent", () => { throw new Error("foo"); }); assert.throws( () => { util.robust_url_decode("%E0%A4%A"); }, {name: "Error", message: "foo"}, ); }); run_test("dumb_strcmp", ({override}) => { override(Intl, "Collator", undefined); const strcmp = util.make_strcmp(); assert.equal(strcmp("a", "b"), -1); assert.equal(strcmp("c", "c"), 0); assert.equal(strcmp("z", "y"), 1); }); run_test("get_edit_event_orig_topic", () => { assert.equal(util.get_edit_event_orig_topic({orig_subject: "lunch"}), "lunch"); }); run_test("is_mobile", () => { window.navigator = {userAgent: "Android"}; assert.ok(util.is_mobile()); window.navigator = {userAgent: "Not mobile"}; assert.ok(!util.is_mobile()); }); run_test("array_compare", () => { assert.ok(util.array_compare([], [])); assert.ok(util.array_compare([1, 2, 3], [1, 2, 3])); assert.ok(!util.array_compare([1, 2], [1, 2, 3])); assert.ok(!util.array_compare([1, 2, 3], [1, 2])); assert.ok(!util.array_compare([1, 2, 3, 4], [1, 2, 3, 5])); }); run_test("normalize_recipients", () => { assert.equal( util.normalize_recipients("ZOE@foo.com, bob@foo.com, alice@foo.com, AARON@foo.com "), "aaron@foo.com,alice@foo.com,bob@foo.com,zoe@foo.com", ); }); run_test("random_int", () => { const min = 0; const max = 100; _.times(500, () => { const val = util.random_int(min, max); assert.ok(min <= val); assert.ok(val <= max); assert.equal(val, Math.floor(val)); }); }); run_test("wildcard_mentions_regexp", () => { const messages_with_all_mentions = [ "@**all**", "some text before @**all** some text after", "@**all** some text after only", "some text before only @**all**", ]; const messages_with_everyone_mentions = [ "@**everyone**", '"@**everyone**"', "@**everyone**: Look at this!", "The <@**everyone**> channel", 'I have to say "@**everyone**" to ding the bell', "some text before @**everyone** some text after", "@**everyone** some text after only", "some text before only @**everyone**", ]; const messages_with_stream_mentions = [ "@**stream**", "some text before @**stream** some text after", "@**stream** some text after only", "some text before only @**stream**", ]; const messages_with_channel_mentions = [ "@**channel**", "some text before @**channel** some text after", "@**channel** some text after only", "some text before only @**channel**", ]; const messages_with_topic_mentions = [ "@**topic**", "some text before @**topic** some text after", "@**topic** some text after only", "some text before only @**topic**", ]; const messages_without_all_mentions = [ "@all", "some text before @all some text after", "`@everyone`", "some_email@everyone.com", "`@**everyone**`", "some_email@**everyone**.com", ]; const messages_without_everyone_mentions = [ "some text before @everyone some text after", "@everyone", "`@everyone`", "some_email@everyone.com", "`@**everyone**`", "some_email@**everyone**.com", ]; const messages_without_stream_mentions = [ "some text before @stream some text after", "@stream", "`@stream`", "some_email@stream.com", "`@**stream**`", "some_email@**stream**.com", ]; const messages_without_channel_mentions = [ "some text before @channel some text after", "@channel", "`@channel`", "some_email@channel.com", "`@**channel**`", "some_email@**channel**.com", ]; let i; for (i = 0; i < messages_with_all_mentions.length; i += 1) { assert.ok(util.find_stream_wildcard_mentions(messages_with_all_mentions[i])); } for (i = 0; i < messages_with_everyone_mentions.length; i += 1) { assert.ok(util.find_stream_wildcard_mentions(messages_with_everyone_mentions[i])); } for (i = 0; i < messages_with_stream_mentions.length; i += 1) { assert.ok(util.find_stream_wildcard_mentions(messages_with_stream_mentions[i])); } for (i = 0; i < messages_with_channel_mentions.length; i += 1) { assert.ok(util.find_stream_wildcard_mentions(messages_with_channel_mentions[i])); } for (i = 0; i < messages_with_topic_mentions.length; i += 1) { assert.ok(!util.find_stream_wildcard_mentions(messages_with_topic_mentions[i])); } for (i = 0; i < messages_without_all_mentions.length; i += 1) { assert.ok(!util.find_stream_wildcard_mentions(messages_without_everyone_mentions[i])); } for (i = 0; i < messages_without_everyone_mentions.length; i += 1) { assert.ok(!util.find_stream_wildcard_mentions(messages_without_everyone_mentions[i])); } for (i = 0; i < messages_without_stream_mentions.length; i += 1) { assert.ok(!util.find_stream_wildcard_mentions(messages_without_stream_mentions[i])); } for (i = 0; i < messages_without_channel_mentions.length; i += 1) { assert.ok(!util.find_stream_wildcard_mentions(messages_without_channel_mentions[i])); } }); run_test("move_array_elements_to_front", () => { const strings = ["string1", "string3", "string2", "string4"]; const strings_selection = ["string4", "string1"]; const strings_expected = ["string1", "string4", "string3", "string2"]; const strings_no_selection = util.move_array_elements_to_front(strings, []); const strings_no_array = util.move_array_elements_to_front([], strings_selection); const strings_actual = util.move_array_elements_to_front(strings, strings_selection); const emails = [ "test@zulip.com", "test@test.com", "test@localhost", "test@invalid@email", "something@zulip.com", ]; const emails_selection = ["test@test.com", "test@localhost", "test@invalid@email"]; const emails_expected = [ "test@test.com", "test@localhost", "test@invalid@email", "test@zulip.com", "something@zulip.com", ]; const emails_actual = util.move_array_elements_to_front(emails, emails_selection); assert.deepEqual(strings_no_selection, strings); assert.deepEqual(strings_no_array, []); assert.deepEqual(strings_actual, strings_expected); assert.deepEqual(emails_actual, emails_expected); }); run_test("filter_by_word_prefix_match", () => { const strings = ["stream-hyphen_underscore/slash", "three word stream"]; const values = [0, 1]; const item_to_string = (idx) => strings[idx]; // Default settings will match words with a space delimiter before them. assert.deepEqual(util.filter_by_word_prefix_match(values, "stream", item_to_string), [0, 1]); assert.deepEqual(util.filter_by_word_prefix_match(values, "word stream", item_to_string), [1]); // Since - appears before `hyphen` in // stream-hyphen_underscore/slash, we require `-` in the set of // characters for it to match. assert.deepEqual(util.filter_by_word_prefix_match(values, "hyphe", item_to_string), []); assert.deepEqual(util.filter_by_word_prefix_match(values, "hyphe", item_to_string, /[\s/_-]/), [ 0, ]); assert.deepEqual(util.filter_by_word_prefix_match(values, "hyphe", item_to_string, /[\s-]/), [ 0, ]); // Similarly `_` must be in the set of allowed characters to match "underscore". assert.deepEqual(util.filter_by_word_prefix_match(values, "unders", item_to_string, /[\s_]/), [ 0, ]); assert.deepEqual(util.filter_by_word_prefix_match(values, "unders", item_to_string, /\s/), []); }); run_test("get_string_diff", () => { assert.deepEqual( util.get_string_diff("#ann is for updates", "#**announce** is for updates"), [1, 4, 13], ); assert.deepEqual(util.get_string_diff("/p", "/poll"), [2, 2, 5]); assert.deepEqual(util.get_string_diff("Hey @Aa", "Hey @**aaron** "), [5, 7, 15]); assert.deepEqual(util.get_string_diff("same", "same"), [0, 0, 0]); assert.deepEqual(util.get_string_diff("same-end", "two same-end"), [0, 0, 4]); assert.deepEqual(util.get_string_diff("space", "sp ace"), [2, 2, 3]); }); run_test("is_valid_url", () => { assert.equal(util.is_valid_url("http://"), false); assert.equal(util.is_valid_url("random_string"), true); assert.equal(util.is_valid_url("http://google.com/something?q=query#hash"), true); assert.equal(util.is_valid_url("/abc/"), true); assert.equal(util.is_valid_url("http://", true), false); assert.equal(util.is_valid_url("random_string", true), false); assert.equal(util.is_valid_url("http://google.com/something?q=query#hash", true), true); assert.equal(util.is_valid_url("/abc/", true), false); }); run_test("format_array_as_list", () => { const array = ["apple", "banana", "orange"]; // when Intl exist assert.equal( util.format_array_as_list(array, "long", "conjunction"), "apple, banana, and orange", ); // when Intl.ListFormat does not exist global.Intl.ListFormat = undefined; assert.equal(util.format_array_as_list(array, "long", "conjunction"), "apple, banana, orange"); }); run_test("get_remaining_time", () => { // When current time is less than start time // Set a random start time const start_time = new Date(1000).getTime(); // Set current time to 400ms ahead of the start time MockDate.set(start_time + 400); const duration = 500; let expected_remaining_time = 100; assert.equal(util.get_remaining_time(start_time, duration), expected_remaining_time); // When current time is greater than start time + duration // Set current time to 100ms after the start time + duration MockDate.set(start_time + duration + 100); expected_remaining_time = 0; assert.equal(util.get_remaining_time(start_time, duration), expected_remaining_time); MockDate.reset(); }); run_test("get_custom_time_in_minutes", () => { const time_input = 15; assert.equal(util.get_custom_time_in_minutes("weeks", time_input), time_input * 7 * 24 * 60); assert.equal(util.get_custom_time_in_minutes("days", time_input), time_input * 24 * 60); assert.equal(util.get_custom_time_in_minutes("hours", time_input), time_input * 60); assert.equal(util.get_custom_time_in_minutes("minutes", time_input), time_input); // Unknown time unit string throws an error, but we still return // the time input that was passed to the function. blueslip.expect("error", "Unexpected custom time unit: invalid"); assert.equal(util.get_custom_time_in_minutes("invalid", time_input), time_input); /// NaN time input returns NaN const invalid_time_input = Number.NaN; assert.equal(util.get_custom_time_in_minutes("hours", invalid_time_input), invalid_time_input); }); run_test("check_and_validate_custom_time_input", () => { const input_is_nan = "24abc"; let checked_input = util.check_time_input(input_is_nan); assert.equal(checked_input, Number.NaN); assert.equal(util.validate_custom_time_input(checked_input), false); const input_is_negative = "-24"; checked_input = util.check_time_input(input_is_negative); assert.equal(checked_input, -24); assert.equal(util.validate_custom_time_input(input_is_negative), false); const input_is_float = "24.5"; checked_input = util.check_time_input(input_is_float); assert.equal(checked_input, 24); checked_input = util.check_time_input(input_is_float, true); assert.equal(checked_input, 24.5); assert.equal(util.validate_custom_time_input(input_is_float), true); const input_is_integer = "10"; checked_input = util.check_time_input(input_is_integer); assert.equal(checked_input, 10); assert.equal(util.validate_custom_time_input(input_is_integer), true); }); run_test("the", () => { const list_with_one_item = ["foo"]; assert.equal(util.the(list_with_one_item), "foo"); blueslip.expect("error", "the: expected only 1 item, got more"); const list_with_more_items = ["foo", "bar"]; // Error is thrown, but we still return the first item to avoid // unnecessarily breaking the app. assert.equal(util.the(list_with_more_items), "foo"); blueslip.expect("error", "the: expected only 1 item, got none"); // Error is thrown, but we still return the "first" item to avoid // unnecessarily breaking the app for places we refactored this that // were previously typed wrong but not breaking the app. assert.equal(util.the([]), undefined); }); run_test("compare_a_b", () => { const user1 = { id: 1, name: "sally", }; const user2 = { id: 2, name: "jenny", }; const user3 = { id: 3, name: "max", }; const user4 = { id: 4, name: "max", }; const unsorted = [user2, user1, user4, user3]; const sorted_by_id = [...unsorted].sort((a, b) => util.compare_a_b(a.id, b.id)); assert.deepEqual(sorted_by_id, [user1, user2, user3, user4]); const sorted_by_name = [...unsorted].sort((a, b) => util.compare_a_b(a.name, b.name)); assert.deepEqual(sorted_by_name, [user2, user4, user3, user1]); });