Files
upstream-zulip/web/tests/compose_reply.test.cjs
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

440 lines
15 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const {zrequire, mock_esm} = require("./lib/namespace.cjs");
const {run_test} = require("./lib/test.cjs");
const compose_reply = zrequire("compose_reply");
const message_store = zrequire("message_store");
const message_fetch_raw_content = mock_esm("../src/message_fetch_raw_content");
const compose_paste = mock_esm("../src/compose_paste");
const message_lists = mock_esm("../src/message_lists");
const pm_user_ids_1 = "1,2";
const pm_user_ids_2 = "3,4";
const msg_pm_1 = {type: "private", to_user_ids: pm_user_ids_1};
const msg_pm_2 = {type: "private", to_user_ids: pm_user_ids_1};
const msg_pm_3 = {type: "private", to_user_ids: pm_user_ids_2};
const msg_stream_denmark_general = {type: "stream", stream_id: 10, topic: "general"};
// Topic case should be ignored by all_messages_have_same_recipient
const msg_stream_denmark_general_caps = {type: "stream", stream_id: 10, topic: "GENERAL"};
const msg_stream_denmark_design = {type: "stream", stream_id: 10, topic: "design"};
const msg_stream_sweden_general = {type: "stream", stream_id: 20, topic: "general"};
function set_message_lists_current(may_contain_multiple) {
message_lists.current = {
data: {
filter: {
may_contain_multiple_conversations() {
return may_contain_multiple;
},
},
},
};
}
run_test("all_messages_have_same_recipient", () => {
// When the filter is set to channel+topic or a dm
// conversation.
set_message_lists_current(false);
assert.equal(
compose_reply.all_messages_have_same_recipient([
msg_stream_denmark_general,
msg_stream_denmark_general_caps,
]),
true,
"Return true if existing narrow cannot possibly have more than one recipients",
);
// Assertions on empty arrays
assert.throws(() => compose_reply.all_messages_have_same_recipient([]), Error);
set_message_lists_current(true);
// Private messages
assert.ok(compose_reply.all_messages_have_same_recipient([msg_pm_1]));
assert.ok(compose_reply.all_messages_have_same_recipient([msg_pm_1, msg_pm_2]));
assert.equal(
compose_reply.all_messages_have_same_recipient([msg_pm_1, msg_pm_2, msg_pm_3]),
false,
"Fails if private message recipients differ",
);
// Channel messages
assert.ok(compose_reply.all_messages_have_same_recipient([msg_stream_denmark_general]));
assert.ok(
compose_reply.all_messages_have_same_recipient([
msg_stream_denmark_general,
msg_stream_denmark_general_caps,
]),
"Topic matching should be case-insensitive",
);
assert.equal(
compose_reply.all_messages_have_same_recipient([
msg_stream_denmark_general,
msg_stream_denmark_design,
]),
false,
"Return false if topics differ",
);
assert.equal(
compose_reply.all_messages_have_same_recipient([
msg_stream_denmark_general,
msg_stream_sweden_general,
]),
false,
"Return false if stream IDs differ for channel messages",
);
// Mixed types
assert.equal(
compose_reply.all_messages_have_same_recipient([msg_pm_1, msg_stream_denmark_general]),
false,
"Return false on mixed stream/private messages",
);
assert.equal(
compose_reply.all_messages_have_same_recipient([msg_stream_denmark_general, msg_pm_1]),
false,
"Return false on mixed stream/private messages",
);
});
run_test("all_messages_have_same_channel", () => {
// Assertions on empty arrays
assert.throws(() => compose_reply.all_messages_have_same_channel([]), Error);
// Messages involving private messages
assert.equal(compose_reply.all_messages_have_same_channel([msg_pm_1, msg_pm_2]), false);
// Channel messages
assert.ok(compose_reply.all_messages_have_same_channel([msg_stream_denmark_general]));
assert.ok(
compose_reply.all_messages_have_same_channel([
msg_stream_denmark_general,
msg_stream_denmark_design,
]),
"Returns true for same stream ID, even if topics differ",
);
assert.equal(
compose_reply.all_messages_have_same_channel([
msg_stream_denmark_general,
msg_stream_sweden_general,
]),
false,
"Return false if stream IDs differ",
);
// Mixed types of messages.
assert.equal(
compose_reply.all_messages_have_same_channel([msg_stream_denmark_general, msg_pm_1]),
false,
);
assert.equal(
compose_reply.all_messages_have_same_channel([msg_pm_1, msg_stream_denmark_general]),
false,
);
});
run_test("all_messages_are_private", () => {
// Assertions on empty arrays
assert.throws(() => compose_reply.all_messages_are_private([]), Error);
// Stream messages immediately return false
assert.equal(compose_reply.all_messages_are_private([msg_stream_denmark_general]), false);
assert.equal(
compose_reply.all_messages_are_private([msg_stream_denmark_general, msg_pm_1]),
false,
);
// Private messages
assert.ok(compose_reply.all_messages_are_private([msg_pm_1]));
assert.ok(
compose_reply.all_messages_are_private([msg_pm_1, msg_pm_3]),
"Returns true for private messages even if recipients differ",
);
// Mixed types where first is private
assert.equal(
compose_reply.all_messages_are_private([msg_pm_1, msg_stream_denmark_general]),
false,
);
});
run_test("get_multi_message_quote_status", () => {
set_message_lists_current(true);
const channel_1_general = {type: "stream", stream_id: 10, topic: "general"};
const channel_1_general_2 = {type: "stream", stream_id: 10, topic: "general"};
const channel_1_design = {type: "stream", stream_id: 10, topic: "design"};
const channel_2_general = {type: "stream", stream_id: 20, topic: "general"};
const pm_1 = {type: "private", to_user_ids: "1,2"};
const pm_1_b = {type: "private", to_user_ids: "1,2"};
const pm_2 = {type: "private", to_user_ids: "3,4"};
assert.equal(
compose_reply.get_multi_message_quote_status([channel_1_general, channel_1_general_2]),
"SAME_RECIPIENT",
);
assert.equal(compose_reply.get_multi_message_quote_status([pm_1, pm_1_b]), "SAME_RECIPIENT");
assert.equal(
compose_reply.get_multi_message_quote_status([channel_1_general, channel_1_design]),
"SAME_CHANNEL_DIFFERENT_TOPICS",
);
assert.equal(
compose_reply.get_multi_message_quote_status([pm_1, pm_2]),
"DIFFERENT_DM_CONVERSATIONS",
);
// NOTHING_IN_COMMON
// 1. Belong to different channels.
assert.equal(
compose_reply.get_multi_message_quote_status([channel_1_general, channel_2_general]),
"NOTHING_IN_COMMON",
);
// 2. Mixed channel and private messages
assert.equal(
compose_reply.get_multi_message_quote_status([channel_1_general, pm_1]),
"NOTHING_IN_COMMON",
);
});
run_test("get_quote_context_for_message", () => {
set_message_lists_current(true);
const msg_alice = {type: "stream", stream_id: 10, topic: "general", sender_id: 1};
const msg_alice_2 = {type: "stream", stream_id: 10, topic: "general", sender_id: 1};
const msg_bob = {type: "stream", stream_id: 10, topic: "general", sender_id: 2};
const msg_bob_design = {type: "stream", stream_id: 10, topic: "design", sender_id: 2};
const pm_alice = {type: "private", to_user_ids: "1,2", sender_id: 1};
const pm_alice_2 = {type: "private", to_user_ids: "1,2", sender_id: 1};
const pm_bob = {type: "private", to_user_ids: "1,2", sender_id: 2};
const pm_charlie = {type: "private", to_user_ids: "1,2,3", sender_id: 3};
// First message in a quote chain
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_alice,
previous_quoted_message: undefined,
forward_message: false,
is_first_in_quote_chain: true,
}),
"INCLUDE_SENDER_AND_RECIPIENT",
);
// Continuous channel message from the same sender
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_alice_2,
previous_quoted_message: msg_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_NOTHING",
);
// Continuous private message from the same sender
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: pm_alice_2,
previous_quoted_message: pm_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_NOTHING",
);
// Same channel thread, but the sender changed
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_bob,
previous_quoted_message: msg_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER",
);
// Same private message thread, but the sender changed
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: pm_bob,
previous_quoted_message: pm_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER",
);
// Channel/topic changed completely
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_bob_design,
previous_quoted_message: msg_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER_AND_RECIPIENT",
);
// Private message recipients changed completely
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: pm_charlie,
previous_quoted_message: pm_alice,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER_AND_RECIPIENT",
);
// Quoted individually (no previous message)
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_alice,
previous_quoted_message: undefined,
forward_message: false,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER",
);
// Forwarded individually
assert.equal(
compose_reply.get_quote_context_for_message({
current_message: msg_alice,
previous_quoted_message: undefined,
forward_message: true,
is_first_in_quote_chain: false,
}),
"INCLUDE_SENDER_AND_RECIPIENT",
);
});
function add_messages_to_message_store(messages) {
message_store.clear_for_testing();
for (const message of messages) {
message_store.update_message_cache({message});
}
}
run_test("build_and_process_quote_assets_for_messages", ({override}) => {
// Case: We get back the raw_content.
const msg_hydrated = {id: 1, raw_content: "Raw markdown", content: "<p>Raw markdown</p>"};
const msg_unhydrated = {id: 2, content: "<p>unhydrated</p>"};
add_messages_to_message_store([msg_hydrated, msg_unhydrated]);
override(
message_fetch_raw_content,
"get_raw_content_for_messages",
({_message_ids, on_success, _on_error}) => {
on_success(["Raw markdown", "hydrated"]);
},
);
let result_assets = [];
compose_reply.build_and_process_quote_assets_for_messages([1, 2], (assets) => {
result_assets = assets;
});
assert.equal(result_assets.length, 2);
assert.deepEqual(
result_assets[0],
{message: msg_hydrated, quote_content: "Raw markdown"},
"Should use raw_content from the fetched results",
);
assert.deepEqual(
result_assets[1],
{message: msg_unhydrated, quote_content: "hydrated"},
"Should use fetched raw_content",
);
// Case: Network error on trying to get raw_content.
// Here, we should use `message.raw_content` if it's available.
// Else we fallback to using the local paste_handler_converter.
override(
message_fetch_raw_content,
"get_raw_content_for_messages",
({_message_ids, _on_success, on_error}) => {
on_error();
},
);
override(
compose_paste,
"paste_handler_converter",
(content) => `converted_by_turndown: ${content}`,
);
compose_reply.build_and_process_quote_assets_for_messages([1, 2], (assets) => {
result_assets = assets;
});
assert.deepEqual(
result_assets[0],
{message: msg_hydrated, quote_content: "Raw markdown"},
"Should use raw_content when available",
);
assert.deepEqual(
result_assets[1],
{message: msg_unhydrated, quote_content: "converted_by_turndown: <p>unhydrated</p>"},
"Fallback to using paste_handler_converter",
);
});
run_test("get_quote_menu_selection", ({override_rewire}) => {
override_rewire(compose_reply, "get_highlighted_message_ids", () => undefined);
assert.deepEqual(compose_reply.get_quote_menu_selection(10), {
kind: "full_message",
hotkeys_agree: true,
});
override_rewire(compose_reply, "get_highlighted_message_ids", () => [10]);
override_rewire(compose_reply, "get_message_selection", () => "partial text");
assert.deepEqual(compose_reply.get_quote_menu_selection(10), {
kind: "message_selection",
quote_content: "partial text",
hotkeys_agree: true,
});
// The selection is inside a different message than the menu was opened
// on, so > and < would act on the selection instead: hide their hints.
assert.deepEqual(compose_reply.get_quote_menu_selection(99), {
kind: "full_message",
hotkeys_agree: false,
});
// Selecting only a sender name or timestamp lands outside
// `.message_content`, so there is nothing quotable to offer.
override_rewire(compose_reply, "get_message_selection", () => " ".repeat(3));
assert.deepEqual(compose_reply.get_quote_menu_selection(10), {
kind: "full_message",
hotkeys_agree: true,
});
// A selection spanning several messages, opened from inside the range;
// we return early without consulting the selected text at all.
override_rewire(compose_reply, "get_highlighted_message_ids", () => [10, 11, 12]);
assert.deepEqual(compose_reply.get_quote_menu_selection(11), {
kind: "selected_messages",
message_ids: [10, 11, 12],
hotkeys_agree: true,
});
// ...and opened from a message outside the range, where the menu acts on
// that one message only and the hotkeys would not.
assert.deepEqual(compose_reply.get_quote_menu_selection(99), {
kind: "full_message",
hotkeys_agree: false,
});
});