zulip/web/tests/typing_status.test.js

422 lines
13 KiB
JavaScript
Raw Normal View History

"use strict";
const assert = require("node:assert/strict");
const {mock_esm, set_global, zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const compose_pm_pill = mock_esm("../src/compose_pm_pill");
const compose_state = mock_esm("../src/compose_state");
const stream_data = mock_esm("../src/stream_data");
const typing = zrequire("typing");
const typing_status = zrequire("../shared/src/typing_status");
const {initialize_user_settings} = zrequire("user_settings");
initialize_user_settings({user_settings: {}});
const TYPING_STARTED_WAIT_PERIOD = 10000;
const TYPING_STOPPED_WAIT_PERIOD = 5000;
function make_time(secs) {
// make times semi-realistic
return 1000000 + 1000 * secs;
}
function returns_time(secs) {
return function () {
return make_time(secs);
};
}
run_test("basics", ({override, override_rewire}) => {
assert.equal(typing_status.state, null);
// invalid conversation basically does nothing
let worker = {};
typing_status.update(worker, null, TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD);
// Start setting up more testing state.
const events = {};
function set_timeout(f, delay) {
assert.equal(delay, 5000);
events.idle_callback = f;
return "idle_timer_stub";
}
function clear_timeout() {
events.timer_cleared = true;
}
set_global("setTimeout", set_timeout);
set_global("clearTimeout", clear_timeout);
function notify_server_start(recipient) {
assert.deepStrictEqual(recipient, {message_type: "direct", ids: [1, 2]});
events.started = true;
}
function notify_server_stop(recipient) {
assert.deepStrictEqual(recipient, {message_type: "direct", ids: [1, 2]});
events.stopped = true;
}
function clear_events() {
events.idle_callback = undefined;
events.started = false;
events.stopped = false;
events.timer_cleared = false;
}
typing_status: Combine two parameters into one, with a maybe-type. The main motivation for this change is to simplify this interface and make it easier to reason about. The case where it affects the behavior is when is_valid_conversation() returns false, while current_recipient and get_recipient() agree on some truthy value. This means the message-content textarea is empty -- in fact the user just cleared it, because we got here from an input event on it -- but the compose box is still open to some PM thread that we have a typing notification still outstanding for. The old behavior is that in this situation we would ignore the fact that the content was empty, and go ahead and prolong the typing notification, by updating our timer and possibly sending a "still typing" notice. This contrasts with the behavior (both old and new) in the case where the content is empty and we *don't* already have an outstanding typing notification, or we have one to some other thread. In that case, we cancel any existing notification and don't start a new one, exactly as if `stop` were called (e.g. because the user closed the compose box.) The new behavior is that we always treat clearing the input as "stopped typing": not only in those cases where we already did, but also in the case where we still have the same recipients. (Which seems like probably the common case.) That seems like the preferable behavior; indeed it's hard to see the point of the "compose_empty" logic if restricted to the other cases. It also makes the interface simpler. Those two properties don't seem like a coincidence, either: the complicated interface made it difficult to unpack exactly what logic we actually had, which made it easy for surprising wrinkles to hang out indefinitely.
2019-10-21 23:37:22 +02:00
function call_handler(new_recipient) {
clear_events();
typing_status.update(
worker,
new_recipient,
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
}
worker = {
get_current_time: returns_time(5),
notify_server_start,
notify_server_stop,
};
// Start talking to users having ids - 1, 2.
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(5 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: false,
});
assert.ok(events.idle_callback);
// type again 3 seconds later
worker.get_current_time = returns_time(8);
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(5 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: false,
stopped: false,
timer_cleared: true,
});
assert.ok(events.idle_callback);
// type after 15 secs, so that we can notify the server
// again
worker.get_current_time = returns_time(18);
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(18 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: true,
});
// Now call recipients idle callback that we captured earlier.
const callback = events.idle_callback;
clear_events();
callback();
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: true,
timer_cleared: true,
});
// Call stop with nothing going on.
call_handler(null);
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: false,
timer_cleared: false,
});
// Start talking to users again.
worker.get_current_time = returns_time(50);
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(50 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: false,
});
assert.ok(events.idle_callback);
// Explicitly stop users.
call_handler(null);
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: true,
timer_cleared: true,
});
// Start talking to users again.
worker.get_current_time = returns_time(80);
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(80 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: false,
});
assert.ok(events.idle_callback);
// Switch to an invalid conversation.
call_handler(null);
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: true,
timer_cleared: true,
});
// Switch to another invalid conversation.
call_handler(null);
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: false,
timer_cleared: false,
});
// Start talking to users again.
worker.get_current_time = returns_time(170);
call_handler({message_type: "direct", ids: [1, 2]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(170 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [1, 2]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: false,
});
assert.ok(events.idle_callback);
// Switch to new users now.
worker.get_current_time = returns_time(171);
worker.notify_server_start = (recipient) => {
assert.deepStrictEqual(recipient, {message_type: "direct", ids: [3, 4]});
events.started = true;
};
call_handler({message_type: "direct", ids: [3, 4]});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(171 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "direct", ids: [3, 4]},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: true,
timer_cleared: true,
});
assert.ok(events.idle_callback);
// test that we correctly detect if worker.get_recipient
// and typing_status.state.current_recipient are the same
override(compose_pm_pill, "get_user_ids_string", () => "1,2,3");
override(compose_state, "get_message_type", () => "private");
typing_status.state.current_recipient = typing.get_recipient();
const call_count = {
maybe_ping_server: 0,
actually_ping_server: 0,
start_or_extend_idle_timer: 0,
stop_last_notification: 0,
};
// stub functions to see how may time they are called
for (const method of Object.keys(call_count)) {
override_rewire(typing_status, method, () => {
call_count[method] += 1;
});
}
// User ids of people in compose narrow doesn't change and is same as state.current_recipient
// so counts of function should increase except stop_last_notification
typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 1);
assert.deepEqual(call_count.start_or_extend_idle_timer, 1);
assert.deepEqual(call_count.stop_last_notification, 0);
typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 2);
assert.deepEqual(call_count.stop_last_notification, 0);
// change in recipient and new_recipient should make us
// call typing_status.stop_last_notification
override(compose_pm_pill, "get_user_ids_string", () => "2,3,4");
typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 3);
assert.deepEqual(call_count.stop_last_notification, 1);
// Stream messages
override(compose_state, "get_message_type", () => "stream");
override(compose_state, "stream_name", () => "Verona");
override(stream_data, "get_stream_id", () => "2");
override(compose_state, "topic", () => "test topic");
typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 4);
assert.deepEqual(call_count.stop_last_notification, 2);
});
run_test("stream_messages", ({override_rewire}) => {
override_rewire(typing_status, "state", null);
let worker = {};
const events = {};
function set_timeout(f, delay) {
assert.equal(delay, 5000);
events.idle_callback = f;
return "idle_timer_stub";
}
function clear_timeout() {
events.timer_cleared = true;
}
set_global("setTimeout", set_timeout);
set_global("clearTimeout", clear_timeout);
function notify_server_start(recipient) {
assert.deepStrictEqual(recipient, {message_type: "stream", stream_id: 3, topic: "test"});
events.started = true;
}
function notify_server_stop(recipient) {
assert.deepStrictEqual(recipient, {message_type: "stream", stream_id: 3, topic: "test"});
events.stopped = true;
}
function clear_events() {
events.idle_callback = undefined;
events.started = false;
events.stopped = false;
events.timer_cleared = false;
}
function call_handler(new_recipient) {
clear_events();
typing_status.update(
worker,
new_recipient,
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
}
worker = {
get_current_time: returns_time(5),
notify_server_start,
notify_server_stop,
};
// Start typing stream message
call_handler({message_type: "stream", stream_id: 3, topic: "test"});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(5 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "stream", stream_id: 3, topic: "test"},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: true,
stopped: false,
timer_cleared: false,
});
assert.ok(events.idle_callback);
// type again 3 seconds later. Covers 'same_stream_and_topic' codepath.
worker.get_current_time = returns_time(8);
call_handler({message_type: "stream", stream_id: 3, topic: "test"});
assert.deepEqual(typing_status.state, {
next_send_start_time: make_time(5 + 10),
idle_timer: "idle_timer_stub",
current_recipient: {message_type: "stream", stream_id: 3, topic: "test"},
});
assert.deepEqual(events, {
idle_callback: events.idle_callback,
started: false,
stopped: false,
timer_cleared: true,
});
assert.ok(events.idle_callback);
// Explicitly stop.
call_handler(null);
assert.deepEqual(typing_status.state, null);
assert.deepEqual(events, {
idle_callback: undefined,
started: false,
stopped: true,
timer_cleared: true,
});
});