zulip/web/tests/message_list_view.test.js

871 lines
33 KiB
JavaScript
Raw Normal View History

"use strict";
const {strict: assert} = require("assert");
const _ = require("lodash");
const {mock_esm, set_global, zrequire} = require("./lib/namespace");
const {run_test, noop} = require("./lib/test");
const $ = require("./lib/zjquery");
set_global("document", "document-stub");
2018-03-21 23:56:30 +01:00
// timerender calls setInterval when imported
mock_esm("../src/timerender", {
render_date(time) {
return [{outerHTML: String(time.getTime())}];
},
stringify_time(time) {
return time.toString("h:mm TT");
},
});
mock_esm("../src/people", {
sender_is_bot: () => false,
sender_is_guest: () => false,
should_add_guest_user_indicator: () => false,
small_avatar_url: () => "fake/small/avatar/url",
});
const {Filter} = zrequire("../src/filter");
const {MessageListView} = zrequire("../src/message_list_view");
const message_list = zrequire("message_list");
const muted_users = zrequire("muted_users");
let next_timestamp = 1500000000;
function test(label, f) {
run_test(label, ({override, mock_template}) => {
muted_users.set_muted_users([]);
mock_template("message_list.hbs", false, () => "<message-list-stub>");
f({override, mock_template});
});
}
test("msg_moved_var", () => {
// This is a test to verify that when the stream or topic is changed
// (and the content is not), the message says "MOVED" rather than "EDITED."
// See the end of the test for the list of cases verified.
function build_message_context(message = {}, message_context = {}) {
message_context = {
...message_context,
};
if ("edit_history" in message) {
message_context.msg = {
last_edit_timestamp: (next_timestamp += 1),
...message,
};
} else {
message_context.msg = {
...message,
};
}
return message_context;
}
function build_message_group(messages) {
return {message_containers: messages};
}
function build_list(message_groups) {
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
function assert_moved_true(message_container) {
assert.equal(message_container.moved, true);
}
function assert_moved_false(message_container) {
assert.equal(message_container.moved, false);
}
function assert_moved_undefined(message_container) {
assert.equal(message_container.moved, undefined);
}
(function test_msg_moved_var() {
const messages = [
// no edit history: NO LABEL
build_message_context({}),
// stream changed: MOVED
build_message_context({
edit_history: [{prev_stream: 1, timestamp: 1000, user_id: 1}],
}),
// topic changed (not resolved/unresolved): MOVED
build_message_context({
edit_history: [
{prev_topic: "test_topic", topic: "new_topic", timestamp: 1000, user_id: 1},
],
}),
// content edited: EDITED
build_message_context({
edit_history: [{prev_content: "test_content", timestamp: 1000, user_id: 1}],
}),
// stream and topic edited: MOVED
build_message_context({
edit_history: [
{
prev_stream: 1,
prev_topic: "test_topic",
topic: "new_topic",
timestamp: 1000,
user_id: 1,
},
],
}),
// topic and content changed: EDITED
build_message_context({
edit_history: [
{
prev_topic: "test_topic",
topic: "new_topic",
prev_content: "test_content",
timestamp: 1000,
user_id: 1,
},
],
}),
// only topic resolved: NO LABEL
build_message_context({
edit_history: [
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1000, user_id: 1},
],
}),
// only topic unresolved: NO LABEL
build_message_context({
edit_history: [
{prev_topic: "✔ test_topic", topic: "test_topic", timestamp: 1000, user_id: 1},
],
}),
// multiple edit history logs, with at least one content edit: EDITED
build_message_context({
edit_history: [
{prev_stream: 1, timestamp: 1000, user_id: 1},
{prev_topic: "old_topic", topic: "test_topic", timestamp: 1001, user_id: 1},
{prev_content: "test_content", timestamp: 1002, user_id: 1},
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1003, user_id: 1},
],
}),
// multiple edit history logs with no content edit: MOVED
build_message_context({
edit_history: [
{prev_stream: 1, timestamp: 1000, user_id: 1},
{prev_topic: "old_topic", topic: "test_topic", timestamp: 1001, user_id: 1},
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1002, user_id: 1},
{prev_topic: "✔ test_topic", topic: "test_topic", timestamp: 1003, user_id: 1},
],
}),
];
const message_group = build_message_group(messages);
const list = build_list([message_group]);
for (const message_container of messages) {
list._maybe_format_me_message(message_container);
list._add_msg_edited_vars(message_container);
}
const result = list._message_groups[0].message_containers;
// no edit history: undefined
assert_moved_undefined(result[0]);
// stream changed: true
assert_moved_true(result[1]);
// topic changed: true
assert_moved_true(result[2]);
// content edited: false
assert_moved_false(result[3]);
// stream and topic edited: true
assert_moved_true(result[4]);
// topic and content changed: false
assert_moved_false(result[5]);
// only topic resolved: undefined
assert_moved_undefined(result[6]);
// only topic unresolved: undefined
assert_moved_undefined(result[7]);
// multiple edits with content edit: false
assert_moved_false(result[8]);
// multiple edits without content edit: true
assert_moved_true(result[9]);
})();
});
test("msg_edited_vars", () => {
// This is a test to verify that only one of the three bools,
// `message_edit_notices_in_left_col`, `message_edit_notices_alongside_sender`,
// `message_edit_notices_for_status_message` is not false; Tests for three
// different kinds of messages:
// * "/me" message
// * message that includes sender
// * message without sender
function build_message_context(message = {}, message_context = {}) {
message_context = {
include_sender: true,
...message_context,
};
message_context.msg = {
is_me_message: false,
last_edit_timestamp: (next_timestamp += 1),
edit_history: [{prev_content: "test_content", timestamp: 1000, user_id: 1}],
...message,
};
return message_context;
}
function build_message_group(messages) {
return {message_containers: messages};
}
function build_list(message_groups) {
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
function assert_left_col(message_container) {
assert.equal(message_container.modified, true);
assert.equal(message_container.message_edit_notices_in_left_col, true);
assert.equal(message_container.message_edit_notices_alongside_sender, false);
assert.equal(message_container.message_edit_notices_for_status_message, false);
}
function assert_alongside_sender(message_container) {
assert.equal(message_container.modified, true);
assert.equal(message_container.message_edit_notices_in_left_col, false);
assert.equal(message_container.message_edit_notices_alongside_sender, true);
assert.equal(message_container.message_edit_notices_for_status_message, false);
}
function assert_status_msg(message_container) {
assert.equal(message_container.modified, true);
assert.equal(message_container.message_edit_notices_in_left_col, false);
assert.equal(message_container.message_edit_notices_alongside_sender, false);
assert.equal(message_container.message_edit_notices_for_status_message, true);
}
function set_edited_notice_locations(message_container) {
const include_sender = message_container.include_sender;
const is_hidden = message_container.is_hidden;
const status_message = Boolean(message_container.status_message);
message_container.message_edit_notices_in_left_col = !include_sender && !is_hidden;
message_container.message_edit_notices_alongside_sender = include_sender && !status_message;
message_container.message_edit_notices_for_status_message =
include_sender && status_message;
}
(function test_msg_edited_vars() {
const messages = [
build_message_context(),
build_message_context({}, {include_sender: false}),
build_message_context({is_me_message: true, content: "<p>/me test</p>"}),
];
const message_group = build_message_group(messages);
const list = build_list([message_group]);
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 message_container of messages) {
list._maybe_format_me_message(message_container);
list._add_msg_edited_vars(message_container);
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
}
const result = list._message_groups[0].message_containers;
set_edited_notice_locations(result[0]);
assert_alongside_sender(result[0]);
set_edited_notice_locations(result[1]);
assert_left_col(result[1]);
set_edited_notice_locations(result[2]);
assert_status_msg(result[2]);
})();
});
test("muted_message_vars", () => {
// This verifies that the variables for muted/hidden messages are set
// correctly.
function build_message_context(message = {}, message_context = {}) {
message_context = {
...message_context,
};
message_context.msg = {
...message,
};
return message_context;
}
function build_message_group(messages) {
return {message_containers: messages};
}
function build_list(message_groups) {
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
function calculate_variables(list, messages, is_revealed) {
list.set_calculated_message_container_variables(messages[0], is_revealed);
list.set_calculated_message_container_variables(messages[1], is_revealed);
list.set_calculated_message_container_variables(messages[2], is_revealed);
return list._message_groups[0].message_containers;
}
(function test_hidden_message_variables() {
// We want to have no search results, which apparently works like this.
// See https://chat.zulip.org/#narrow/channel/6-frontend/topic/set_find_results.20with.20no.20results/near/1414799
const empty_list_stub = $.create("empty-stub", {children: []});
$("<message-stub-1>").set_find_results(".user-mention:not(.silent)", empty_list_stub);
$("<message-stub2>").set_find_results(".user-mention:not(.silent)", empty_list_stub);
$("<message-stub-3>").set_find_results(".user-mention:not(.silent)", empty_list_stub);
// Make a representative message group of three messages.
const messages = [
build_message_context(
{sender_id: 10, content: "<message-stub-1>"},
{include_sender: true},
),
build_message_context(
{mentioned: true, sender_id: 10, content: "<message-stub2>"},
{include_sender: false},
),
build_message_context(
{sender_id: 10, content: "<message-stub-3>"},
{include_sender: false},
),
];
const message_group = build_message_group(messages);
const list = build_list([message_group]);
list._add_msg_edited_vars = noop;
// Sender is not muted.
let result = calculate_variables(list, messages);
// sanity check on mocked values
assert.equal(result[1].sender_is_bot, false);
assert.equal(result[1].sender_is_guest, false);
assert.equal(result[1].small_avatar_url, "fake/small/avatar/url");
// Check that `is_hidden` is false on all messages, and `include_sender` has not changed.
assert.equal(result[0].is_hidden, false);
assert.equal(result[1].is_hidden, false);
assert.equal(result[2].is_hidden, false);
assert.equal(result[0].include_sender, true);
assert.equal(result[1].include_sender, false);
assert.equal(result[2].include_sender, false);
// Additionally test that the message with a mention is marked as such.
assert.equal(result[1].mention_classname, "group_mention");
// Now, mute the sender.
muted_users.add_muted_user(10);
result = calculate_variables(list, messages);
// Check that `is_hidden` is true and `include_sender` is false on all messages.
assert.equal(result[0].is_hidden, true);
assert.equal(result[1].is_hidden, true);
assert.equal(result[2].is_hidden, true);
assert.equal(result[0].include_sender, false);
assert.equal(result[1].include_sender, false);
assert.equal(result[2].include_sender, false);
// Additionally test that, both there is no mention classname even on that message
// which has a mention, since we don't want to display muted mentions so visibly.
assert.equal(result[1].mention_classname, null);
// Now, reveal the hidden messages.
let is_revealed = true;
result = calculate_variables(list, messages, is_revealed);
// Check that `is_hidden` is false and `include_sender` is true on all messages.
assert.equal(result[0].is_hidden, false);
assert.equal(result[1].is_hidden, false);
assert.equal(result[2].is_hidden, false);
assert.equal(result[0].include_sender, true);
assert.equal(result[1].include_sender, true);
assert.equal(result[2].include_sender, true);
// Additionally test that the message with a mention is marked as such.
assert.equal(result[1].mention_classname, "group_mention");
2021-10-18 16:30:46 +02:00
// Now test rehiding muted user's message
is_revealed = false;
result = calculate_variables(list, messages, is_revealed);
// Check that `is_hidden` is false and `include_sender` is false on all messages.
assert.equal(result[0].is_hidden, true);
assert.equal(result[1].is_hidden, true);
assert.equal(result[2].is_hidden, true);
assert.equal(result[0].include_sender, false);
assert.equal(result[1].include_sender, false);
assert.equal(result[2].include_sender, false);
// Additionally test that, both there is no mention classname even on that message
// which has a mention, since we don't want to display hidden mentions so visibly.
assert.equal(result[1].mention_classname, null);
})();
});
test("merge_message_groups", ({mock_template}) => {
mock_template("message_list.hbs", false, () => "<message-list-stub>");
// MessageListView has lots of DOM code, so we are going to test the message
// group merging logic on its own.
function build_message_context(message = {}, message_context = {}) {
message_context = {
include_sender: true,
...message_context,
};
message_context.msg = {
id: _.uniqueId("test_message_"),
status_message: false,
type: "stream",
stream_id: 2,
topic: "Test topic 1",
sender_email: "test@example.com",
timestamp: (next_timestamp += 1),
...message,
};
return message_context;
}
function build_message_group(messages) {
return {
message_containers: messages,
message_group_id: _.uniqueId("test_message_group_"),
};
}
function build_list(message_groups) {
const filter = new Filter([{operator: "stream", operand: "foo"}]);
const list = new message_list.MessageList({
filter,
is_node_test: true,
});
const view = new MessageListView(list, true, true);
view._message_groups = message_groups;
view.list.unsubscribed_bookend_content = noop;
view.list.subscribed_bookend_content = noop;
return view;
}
function extract_message_ids(lst) {
return lst.map((item) => item.msg.id);
}
function assert_message_list_equal(list1, list2) {
const ids1 = extract_message_ids(list1);
const ids2 = extract_message_ids(list2);
assert.ok(ids1.length);
assert.deepEqual(ids1, ids2);
}
function extract_group(group) {
return extract_message_ids(group.message_containers);
}
function assert_message_groups_list_equal(list1, list2) {
const ids1 = list1.map((group) => extract_group(group));
const ids2 = list2.map((group) => extract_group(group));
assert.ok(ids1.length);
assert.deepEqual(ids1, ids2);
}
(function test_empty_list_bottom() {
const list = build_list([]);
const message_group = build_message_group([build_message_context()]);
const result = list.merge_message_groups([message_group], "bottom");
assert_message_groups_list_equal(list._message_groups, [message_group]);
assert_message_groups_list_equal(result.append_groups, [message_group]);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
})();
(function test_append_message_same_topic() {
const message1 = build_message_context();
const message_group1 = build_message_group([message1]);
const message2 = build_message_context();
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert_message_groups_list_equal(list._message_groups, [
build_message_group([message1, message2]),
]);
assert.deepEqual(result.append_groups, []);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert_message_list_equal(result.append_messages, [message2]);
})();
(function test_append_message_different_topic() {
const message1 = build_message_context();
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({topic: "Test topic 2"});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert.ok(!message_group2.date_unchanged, true);
assert_message_groups_list_equal(list._message_groups, [message_group1, message_group2]);
assert_message_groups_list_equal(result.append_groups, [message_group2]);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
})();
(function test_append_message_different_topic_and_days() {
const message1 = build_message_context({timestamp: 1000});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({topic: "Test topic 2", timestamp: 900000});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert_message_groups_list_equal(list._message_groups, [message_group1, message_group2]);
assert_message_groups_list_equal(result.append_groups, [message_group2]);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
assert.equal(message_group2.date_unchanged, false);
})();
(function test_append_message_different_day() {
const message1 = build_message_context({timestamp: 1000});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({timestamp: 900000});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert_message_groups_list_equal(list._message_groups, [message_group1]);
assert.deepEqual(result.append_groups, []);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, [message2]);
assert.ok(list._message_groups[0].message_containers[1].want_date_divider);
})();
(function test_append_message_historical() {
const message1 = build_message_context({historical: false});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({historical: true});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert.ok(message_group2.bookend_top);
assert_message_groups_list_equal(list._message_groups, [message_group1, message_group2]);
assert_message_groups_list_equal(result.append_groups, [message_group2]);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
})();
(function test_append_message_same_topic_me_message() {
const message1 = build_message_context();
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({is_me_message: true});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "bottom");
assert.ok(message2.include_sender);
assert_message_groups_list_equal(list._message_groups, [
build_message_group([message1, message2]),
]);
assert.deepEqual(result.append_groups, []);
assert.deepEqual(result.prepend_groups, []);
assert.deepEqual(result.rerender_groups, []);
assert_message_list_equal(result.append_messages, [message2]);
})();
(function test_prepend_message_same_topic() {
const message1 = build_message_context();
const message_group1 = build_message_group([message1]);
const message2 = build_message_context();
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "top");
assert_message_groups_list_equal(list._message_groups, [
build_message_group([message2, message1]),
]);
assert.deepEqual(result.append_groups, []);
assert.deepEqual(result.prepend_groups, []);
assert_message_groups_list_equal(result.rerender_groups, [
build_message_group([message2, message1]),
]);
assert.deepEqual(result.append_messages, []);
})();
(function test_prepend_message_different_topic() {
const message1 = build_message_context();
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({topic: "Test topic 2"});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "top");
assert_message_groups_list_equal(list._message_groups, [message_group2, message_group1]);
assert.deepEqual(result.append_groups, []);
assert_message_groups_list_equal(result.prepend_groups, [message_group2]);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
})();
(function test_prepend_message_different_topic_and_day() {
const message1 = build_message_context({timestamp: 900000});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({topic: "Test topic 2", timestamp: 1000});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "top");
assert.equal(message_group1.date_unchanged, false);
assert_message_groups_list_equal(list._message_groups, [message_group2, message_group1]);
assert.deepEqual(result.append_groups, []);
assert_message_groups_list_equal(result.prepend_groups, [message_group2]);
assert.deepEqual(result.rerender_groups, [message_group1]);
assert.deepEqual(result.append_messages, []);
})();
(function test_prepend_message_different_day() {
const message1 = build_message_context({timestamp: 900000});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({timestamp: 1000});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "top");
assert.equal(message_group2.message_containers[1].date_divider_html, "900000000");
assert_message_groups_list_equal(list._message_groups, [message_group2]);
assert.deepEqual(result.append_groups, []);
assert.deepEqual(result.prepend_groups, []);
assert_message_groups_list_equal(result.rerender_groups, [message_group2]);
assert.deepEqual(result.append_messages, []);
})();
(function test_prepend_message_historical() {
const message1 = build_message_context({historical: false});
const message_group1 = build_message_group([message1]);
const message2 = build_message_context({historical: true});
const message_group2 = build_message_group([message2]);
const list = build_list([message_group1]);
const result = list.merge_message_groups([message_group2], "top");
assert.ok(message_group1.bookend_top);
assert_message_groups_list_equal(list._message_groups, [message_group2, message_group1]);
assert.deepEqual(result.append_groups, []);
assert_message_groups_list_equal(result.prepend_groups, [message_group2]);
assert.deepEqual(result.rerender_groups, []);
assert.deepEqual(result.append_messages, []);
})();
});
2018-03-21 23:56:30 +01:00
test("render_windows", ({mock_template}) => {
mock_template("message_list.hbs", false, () => "<message-list-stub>");
2018-03-21 23:56:30 +01:00
// We only render up to 400 messages at a time in our message list,
// and we only change the window (which is a range, really, with
// start/end) when the pointer moves outside of the window or close
// to the edges.
const view = (function make_view() {
const filter = new Filter([]);
2018-03-21 23:56:30 +01:00
const list = new message_list.MessageList({
filter,
is_node_test: true,
});
const view = list.view;
2018-03-21 23:56:30 +01:00
// Stub out functionality that is not core to the rendering window
// logic.
list.data.unmuted_messages = (messages) => messages;
2018-03-21 23:56:30 +01:00
// We don't need to actually render the DOM. The windowing logic
// sits above that layer.
view.render = noop;
view.rerender_preserving_scrolltop = noop;
return view;
})();
2018-03-21 23:56:30 +01:00
const list = view.list;
2018-03-21 23:56:30 +01:00
(function test_with_empty_list() {
// The function should early exit here.
const rendered = view.maybe_rerender();
2018-03-21 23:56:30 +01:00
assert.equal(rendered, false);
})();
2018-03-21 23:56:30 +01:00
let messages;
2018-03-21 23:56:30 +01:00
function reset_list(opts) {
messages = _.range(opts.count).map((i) => ({
js: Convert _.map(a, …) to a.map(…). And convert the corresponding function expressions to arrow style while we’re here. 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 K from "ast-types/gen/kinds"; 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); 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; recast.visit(ast, { visitCallExpression(path) { const { callee, arguments: args } = path.node; if ( n.MemberExpression.check(callee) && !callee.computed && n.Identifier.check(callee.object) && callee.object.name === "_" && n.Identifier.check(callee.property) && callee.property.name === "map" && args.length === 2 && checkExpression(args[0]) && checkExpression(args[1]) ) { const [arr, fn] = args; path.replace( b.callExpression(b.memberExpression(arr, b.identifier("map")), [ n.FunctionExpression.check(fn) || n.ArrowFunctionExpression.check(fn) ? b.arrowFunctionExpression( fn.params, n.BlockStatement.check(fn.body) && fn.body.body.length === 1 && n.ReturnStatement.check(fn.body.body[0]) ? fn.body.body[0].argument || b.identifier("undefined") : fn.body ) : fn, ]) ); changed = true; } this.traverse(path); }, }); 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-08 02:43:49 +01:00
id: i,
}));
list.selected_idx = () => 0;
list.view.clear_table = noop;
2018-03-21 23:56:30 +01:00
list.clear();
list.add_messages(messages, {});
}
function verify_no_move_range(start, end) {
// In our render window, there are up to 150 positions in
// the list (with potentially 50 at the start if the range
// starts with 0) where we can move the pointer without forcing
2018-03-21 23:56:30 +01:00
// a re-render. The code avoids hasty re-renders for
// performance reasons.
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 idx of _.range(start, end)) {
list.selected_idx = () => idx;
const rendered = view.maybe_rerender();
2018-03-21 23:56:30 +01:00
assert.equal(rendered, 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
}
2018-03-21 23:56:30 +01:00
}
function verify_move(idx, range) {
const start = range[0];
const end = range[1];
2018-03-21 23:56:30 +01:00
list.selected_idx = () => idx;
const rendered = view.maybe_rerender();
2018-03-21 23:56:30 +01:00
assert.equal(rendered, true);
assert.equal(view._render_win_start, start);
assert.equal(view._render_win_end, end);
}
function verify_move_and_no_move_range(move_target, opts = {}) {
// When we move to position X, we expect 250/2 = 125 messages on
// either side, unless that goes outside the `count`, in which
// case we'll specify it in `opts`.
const move_start = opts.move_start ?? move_target - 125;
const move_end = opts.move_end ?? move_target + 125;
verify_move(move_target, [move_start, move_end]);
// the no-move range is a 50 buffer on each side
const no_move_start = opts.no_move_start ?? move_start + 50;
const no_move_end = move_end - 50;
verify_no_move_range(no_move_start, no_move_end);
}
2018-03-21 23:56:30 +01:00
reset_list({count: 51});
verify_no_move_range(0, 51); // This is the whole list
2018-03-21 23:56:30 +01:00
// Start a new list with more messages. Note that the order of
// these checks matters; each time we call `verify_move` or
// `verify_move_and_no_move_range`, we are moving the currently
// selected position in the list.
2018-03-21 23:56:30 +01:00
reset_list({count: 450});
// 250 messages rendered, with the last 50 in the move range
verify_no_move_range(0, 200);
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(350, {
// top maxes out at 450
move_end: 450,
});
2018-03-21 23:56:30 +01:00
// We load more than 125 on the upper end, because we load the full 250
// messages and 124 is less than half of that.
verify_move_and_no_move_range(124, {
move_start: 0,
move_end: 250,
});
2018-03-21 23:56:30 +01:00
// If we now jump to a message ID close enough to the end of the
// range, the render window is limited.
verify_move_and_no_move_range(350, {
move_end: 450,
});
2018-03-21 23:56:30 +01:00
// Now jump the selected ID close to the start again.
verify_move_and_no_move_range(124, {
move_start: 0,
move_end: 250,
// The first 50 aren't in a move range, because we can't load earlier
// messages than 0.
no_move_start: 0,
});
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(400, {
// top maxes out at 450
move_end: 450,
});
2018-03-21 23:56:30 +01:00
reset_list({count: 800});
verify_no_move_range(0, 200);
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(350);
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(500);
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(750, {
// top maxes out at 800
move_end: 800,
});
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(499);
2018-03-21 23:56:30 +01:00
verify_move_and_no_move_range(348);
2018-03-21 23:56:30 +01:00
// We load more than 125 on the upper end, because we load the full 250
// messages and 122 is less than half of that.
verify_move_and_no_move_range(122, {
move_start: 0,
move_end: 250,
no_move_start: 0,
});
});