zulip/web/tests/message_list_data.test.js

317 lines
11 KiB
JavaScript
Raw Normal View History

"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const blueslip = require("./lib/zblueslip");
const user_topics = zrequire("user_topics");
const muted_users = zrequire("muted_users");
const {MessageListData} = zrequire("../src/message_list_data");
const {Filter} = zrequire("filter");
function make_msg(msg_id) {
return {
id: msg_id,
type: "stream",
unread: true,
topic: "whatever",
};
}
function make_msgs(msg_ids) {
return msg_ids.map((msg_id) => make_msg(msg_id));
}
function assert_contents(mld, msg_ids) {
const msgs = mld.all_messages();
assert.deepEqual(msgs, make_msgs(msg_ids));
}
function assert_msg_ids(messages, msg_ids) {
assert.deepEqual(
msg_ids,
messages.map((message) => message.id),
);
}
run_test("basics", () => {
const mld = new MessageListData({
excludes_muted_topics: false,
filter: new Filter(),
});
assert.equal(mld.is_search(), false);
assert.ok(mld.can_mark_messages_read());
mld.add_anywhere(make_msgs([35, 25, 15, 45]));
assert_contents(mld, [15, 25, 35, 45]);
const new_msgs = make_msgs([10, 20, 30, 40, 50, 60, 70]);
const info = mld.add_messages(new_msgs);
assert.deepEqual(info, {
top_messages: make_msgs([10]),
interior_messages: make_msgs([20, 30, 40]),
bottom_messages: make_msgs([50, 60, 70]),
});
assert_contents(mld, [10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70]);
assert.equal(mld.selected_id(), -1);
assert.equal(mld.closest_id(8), 10);
assert.equal(mld.closest_id(27), 25);
assert.equal(mld.closest_id(72), 70);
mld.set_selected_id(50);
assert.equal(mld.selected_id(), 50);
assert.equal(mld.selected_idx(), 8);
mld.remove([50]);
assert_contents(mld, [10, 15, 20, 25, 30, 35, 40, 45, 60, 70]);
mld.update_items_for_muting();
assert_contents(mld, [10, 15, 20, 25, 30, 35, 40, 45, 60, 70]);
mld.reset_select_to_closest();
assert.equal(mld.selected_id(), 45);
assert.equal(mld.selected_idx(), 7);
assert.equal(mld.first_unread_message_id(), 10);
assert.equal(mld.has_unread_messages(), true);
mld.get(10).unread = false;
assert.equal(mld.first_unread_message_id(), 15);
assert.equal(mld.has_unread_messages(), true);
mld.clear();
assert_contents(mld, []);
const msgs_sent_by_6 = [
{id: 2, sender_id: 6, type: "stream", stream_id: 1, topic: "whatever"},
{id: 4, sender_id: 6, type: "private", to_user_ids: "6,9,10"},
{id: 6, sender_id: 6, type: "private", to_user_ids: "6, 11"},
];
const msgs_with_sender_ids = [
{id: 1, sender_id: 1, type: "stream", stream_id: 1, topic: "random1"},
{id: 3, sender_id: 4, type: "stream", stream_id: 1, topic: "random2"},
{id: 5, sender_id: 2, type: "private", to_user_ids: "2,10,11"},
{id: 8, sender_id: 11, type: "private", to_user_ids: "10"},
{id: 9, sender_id: 11, type: "private", to_user_ids: "9"},
...msgs_sent_by_6,
];
mld.add_messages(msgs_with_sender_ids);
assert.deepEqual(mld.get_messages_sent_by_user(6), msgs_sent_by_6);
mld.clear();
assert_contents(mld, []);
assert.equal(mld.closest_id(99), -1);
assert.equal(mld.get_last_message_sent_by_me(), undefined);
mld.add_messages(make_msgs([120, 125.01, 130, 140]));
assert_contents(mld, [120, 125.01, 130, 140]);
mld.set_selected_id(125.01);
assert.equal(mld.selected_id(), 125.01);
mld.get(125.01).id = 145;
mld.change_message_id(125.01, 145);
assert_contents(mld, [120, 130, 140, 145]);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const msg of mld.all_messages()) {
msg.unread = false;
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
assert.equal(mld.first_unread_message_id(), 145);
assert.equal(mld.has_unread_messages(), false);
});
run_test("muting", () => {
let mld = new MessageListData({
excludes_muted_topics: false,
filter: new Filter([{operator: "dm", operand: "alice@example.com"}]),
});
const msgs = [
{id: 1, type: "stream", stream_id: 1, topic: "muted"},
{id: 2, type: "stream", stream_id: 1, topic: "whatever"},
// mentions override muting
{id: 3, type: "stream", stream_id: 1, topic: "muted", mentioned: true},
// 10 = muted user, 9 = non-muted user, 11 = you
// muted to group direct message
{id: 4, type: "private", to_user_ids: "9,10,11", sender_id: 10},
// non-muted to group direct message
{id: 5, type: "private", to_user_ids: "9,10,11", sender_id: 9},
// muted to 1:1 direct message
{id: 6, type: "private", to_user_ids: "11", sender_id: 10},
// non-muted to 1:1 direct message
{id: 7, type: "private", to_user_ids: "11", sender_id: 9},
// 1:1 direct message to muted
{id: 8, type: "private", to_user_ids: "10", sender_id: 11},
// 1:1 direct message to non-muted
{id: 9, type: "private", to_user_ids: "9", sender_id: 11},
];
user_topics.update_user_topics(1, "muted", user_topics.all_visibility_policies.MUTED);
muted_users.add_muted_user(10);
// `messages_filtered_for_topic_mutes` should skip filtering
// messages if `excludes_muted_topics` is false.
assert.deepEqual(mld.messages_filtered_for_topic_mutes(msgs), msgs);
// If we are in a 1:1 direct message narrow, `messages_filtered_for_user_mutes`
// should skip filtering messages.
assert.deepEqual(mld.messages_filtered_for_user_mutes(msgs), msgs);
// Test actual behaviour of `messages_filtered_for_*` methods.
mld.excludes_muted_topics = true;
mld.filter = new Filter([{operator: "stream", operand: "general"}]);
const res = mld.messages_filtered_for_topic_mutes(msgs);
assert.deepEqual(res, [
{id: 2, type: "stream", stream_id: 1, topic: "whatever"},
{id: 3, type: "stream", stream_id: 1, topic: "muted", mentioned: true}, // mentions override muting
// `messages_filtered_for_topic_mutes` does not affect direct messages
{id: 4, type: "private", to_user_ids: "9,10,11", sender_id: 10},
{id: 5, type: "private", to_user_ids: "9,10,11", sender_id: 9},
{id: 6, type: "private", to_user_ids: "11", sender_id: 10},
{id: 7, type: "private", to_user_ids: "11", sender_id: 9},
{id: 8, type: "private", to_user_ids: "10", sender_id: 11},
{id: 9, type: "private", to_user_ids: "9", sender_id: 11},
]);
const res_user = mld.messages_filtered_for_user_mutes(msgs);
assert.deepEqual(res_user, [
// `messages_filtered_for_user_mutes` does not affect stream messages
{id: 1, type: "stream", stream_id: 1, topic: "muted"},
{id: 2, type: "stream", stream_id: 1, topic: "whatever"},
{id: 3, type: "stream", stream_id: 1, topic: "muted", mentioned: true},
// muted to group direct message
{id: 4, type: "private", to_user_ids: "9,10,11", sender_id: 10},
// non-muted to group direct message
{id: 5, type: "private", to_user_ids: "9,10,11", sender_id: 9},
// non-muted to 1:1 direct message
{id: 7, type: "private", to_user_ids: "11", sender_id: 9},
// 1:1 direct message to non-muted
{id: 9, type: "private", to_user_ids: "9", sender_id: 11},
]);
// Output filtered based on both topic and user muting.
mld._all_items = msgs;
const filtered_messages = mld.unmuted_messages(mld._all_items);
assert.deepEqual(filtered_messages, [
{id: 2, type: "stream", stream_id: 1, topic: "whatever"},
{id: 3, type: "stream", stream_id: 1, topic: "muted", mentioned: true},
{id: 4, type: "private", to_user_ids: "9,10,11", sender_id: 10},
{id: 5, type: "private", to_user_ids: "9,10,11", sender_id: 9},
{id: 7, type: "private", to_user_ids: "11", sender_id: 9},
{id: 9, type: "private", to_user_ids: "9", sender_id: 11},
]);
// Also verify that, the correct set of messages is stored in `_items`
// once we update the list for muting.
mld.update_items_for_muting();
assert.deepEqual(filtered_messages, mld._items);
// MessageListData methods should always attempt to filter messages,
// and keep `_all_items` up-to-date.
mld = new MessageListData({
excludes_muted_topics: true,
filter: new Filter(),
});
assert.deepEqual(mld._all_items, []);
let unmuted_messages_calls = 0;
mld.unmuted_messages = (messages) => {
unmuted_messages_calls = unmuted_messages_calls + 1;
return messages;
};
mld.add_anywhere([{id: 10}, {id: 20}]);
assert.equal(unmuted_messages_calls, 1);
assert_msg_ids(mld._all_items, [10, 20]);
mld.prepend([{id: 9}, {id: 19}]);
assert.equal(unmuted_messages_calls, 2);
assert_msg_ids(mld._all_items, [9, 19, 10, 20]);
mld.append([{id: 11}, {id: 21}]);
assert.equal(unmuted_messages_calls, 3);
assert_msg_ids(mld._all_items, [9, 19, 10, 20, 11, 21]);
mld.remove([9]);
assert_msg_ids(mld._all_items, [19, 10, 20, 11, 21]);
mld.reorder_messages(20);
assert_msg_ids(mld._all_items, [10, 11, 19, 20, 21]);
mld.clear();
assert_msg_ids(mld._all_items, []);
// Test `add_messages` populates the `info` dict **after**
// filtering the messages.
mld = new MessageListData({
excludes_muted_topics: true,
filter: new Filter(),
});
const orig_messages = [
{id: 3, type: "stream", stream_id: 1, topic: "muted"},
{id: 4, type: "stream", stream_id: 1, topic: "whatever"},
{id: 7, type: "stream", stream_id: 1, topic: "muted"},
{id: 8, type: "stream", stream_id: 1, topic: "whatever"},
];
const orig_info = mld.add_messages(orig_messages);
assert.deepEqual(orig_info, {
top_messages: [],
interior_messages: [],
bottom_messages: [
{id: 4, type: "stream", stream_id: 1, topic: "whatever"},
{id: 8, type: "stream", stream_id: 1, topic: "whatever"},
],
});
assert_msg_ids(mld._all_items, [3, 4, 7, 8]);
assert_msg_ids(mld._items, [4, 8]);
const more_messages = [
{id: 1, type: "stream", stream_id: 1, topic: "muted"},
{id: 2, type: "stream", stream_id: 1, topic: "whatever"},
{id: 3, type: "stream", stream_id: 1, topic: "muted"}, // dup
{id: 5, type: "stream", stream_id: 1, topic: "muted"},
{id: 6, type: "stream", stream_id: 1, topic: "whatever"},
{id: 9, type: "stream", stream_id: 1, topic: "muted"},
{id: 10, type: "stream", stream_id: 1, topic: "whatever"},
];
const more_info = mld.add_messages(more_messages);
assert_msg_ids(mld._all_items, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert_msg_ids(mld._items, [2, 4, 6, 8, 10]);
assert.deepEqual(more_info, {
top_messages: [{id: 2, type: "stream", stream_id: 1, topic: "whatever"}],
interior_messages: [{id: 6, type: "stream", stream_id: 1, topic: "whatever"}],
bottom_messages: [{id: 10, type: "stream", stream_id: 1, topic: "whatever"}],
});
});
run_test("errors", () => {
const mld = new MessageListData({
excludes_muted_topics: false,
filter: new Filter(),
});
assert.equal(mld.get("bogus-id"), undefined);
assert.throws(
() => {
mld._add_to_hash(["asdf"]);
},
{message: "Bad message id"},
);
blueslip.expect("error", "Duplicate message added to MessageListData");
mld._hash.set(1, "taken");
mld._add_to_hash(make_msgs([1]));
});