mirror of https://github.com/zulip/zulip.git
node tests: Split up tutorial into smaller examples.
I didn't bother to keep the very last test from tutorial, which has some complications related to clearing stream_list data and was kind of overkill.
This commit is contained in:
parent
bbec2d9625
commit
198c67a9bd
|
@ -44,7 +44,8 @@ there are, you should strive to follow the patterns of the existing tests
|
|||
and add your own tests.
|
||||
|
||||
A good first test to read is
|
||||
[tutorial.js](https://github.com/zulip/zulip/blob/master/frontend_tests/node_tests/tutorial.js).
|
||||
[example1.js](https://github.com/zulip/zulip/blob/master/frontend_tests/node_tests/example1.js).
|
||||
(And then there are several other example files.)
|
||||
|
||||
## How the node tests work
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
// This is a general tour of how to write node tests that
|
||||
// may also give you some quick insight on how the Zulip
|
||||
// browser app is constructed.
|
||||
|
||||
// The statements below are pretty typical for most node
|
||||
// tests. The reason we need these helpers will hopefully
|
||||
// become clear as you keep reading.
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
// We will use our special zrequire helper to import the
|
||||
// Zulip code. We use zrequire instead of require,
|
||||
// because it has some magic to clear state when we move
|
||||
// on to the next test.
|
||||
const people = zrequire("people");
|
||||
const stream_data = zrequire("stream_data");
|
||||
const util = zrequire("util");
|
||||
|
||||
// Let's start with testing a function from util.js.
|
||||
//
|
||||
// The most basic unit tests load up code, call functions,
|
||||
// and assert truths:
|
||||
|
||||
assert(!util.find_wildcard_mentions("boring text"));
|
||||
assert(util.find_wildcard_mentions("mention @**everyone**"));
|
||||
|
||||
// Let's test with people.js next. We'll show this technique:
|
||||
// * get a false value
|
||||
// * change the data
|
||||
// * get a true value
|
||||
|
||||
const isaac = {
|
||||
email: "isaac@example.com",
|
||||
user_id: 30,
|
||||
full_name: "Isaac Newton",
|
||||
};
|
||||
|
||||
// The `people`object is a very fundamental object in the
|
||||
// Zulip app. You can learn a lot more about it by reading
|
||||
// the tests in people.js in the same directory as this file.
|
||||
|
||||
// Let's exercise the code and use assert to verify it works!
|
||||
assert(!people.is_known_user_id(isaac.user_id));
|
||||
people.add_active_user(isaac);
|
||||
assert(people.is_known_user_id(isaac.user_id));
|
||||
|
||||
// Let's look at stream_data next, and we will start by putting
|
||||
// some data at module scope. (You could also declare this inside
|
||||
// the test, if you prefer.)
|
||||
|
||||
const denmark_stream = {
|
||||
color: "blue",
|
||||
name: "Denmark",
|
||||
stream_id: 101,
|
||||
subscribed: false,
|
||||
};
|
||||
|
||||
// We introduce the run_test helper, which mostly just causes
|
||||
// a line of output to go to the console. It does a little more than
|
||||
// that, which we will see later.
|
||||
|
||||
run_test("verify stream_data persists stream color", () => {
|
||||
stream_data.clear_subscriptions();
|
||||
assert.equal(stream_data.get_sub_by_name("Denmark"), undefined);
|
||||
stream_data.add_sub(denmark_stream);
|
||||
const sub = stream_data.get_sub_by_name("Denmark");
|
||||
assert.equal(sub.color, "blue");
|
||||
});
|
||||
// See example2.js in this directory.
|
|
@ -0,0 +1,99 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
// Hopefully the basic patterns for testing data-oriented modules
|
||||
// are starting to become apparent. To reinforce that, we will present
|
||||
// few more examples that also expose you to some of our core
|
||||
// data objects. Also, we start testing some objects that have
|
||||
// deeper dependencies.
|
||||
|
||||
const message_store = zrequire("message_store");
|
||||
const people = zrequire("people");
|
||||
const stream_data = zrequire("stream_data");
|
||||
const stream_topic_history = zrequire("stream_topic_history");
|
||||
const unread = zrequire("unread");
|
||||
|
||||
// It's typical to set up a little bit of data at the top of a
|
||||
// test module, but you can also do this within tests. Here we
|
||||
// will set up things at the top.
|
||||
|
||||
const isaac = {
|
||||
email: "isaac@example.com",
|
||||
user_id: 30,
|
||||
full_name: "Isaac Newton",
|
||||
};
|
||||
|
||||
const denmark_stream = {
|
||||
color: "blue",
|
||||
name: "Denmark",
|
||||
stream_id: 101,
|
||||
subscribed: false,
|
||||
};
|
||||
|
||||
const messages = {
|
||||
isaac_to_denmark_stream: {
|
||||
id: 400,
|
||||
sender_id: isaac.user_id,
|
||||
stream_id: denmark_stream.stream_id,
|
||||
type: "stream",
|
||||
flags: ["has_alert_word"],
|
||||
topic: "copenhagen",
|
||||
// note we don't have every field that a "real" message
|
||||
// would have, and that can be fine
|
||||
},
|
||||
};
|
||||
|
||||
// We aren't going to modify isaac in our tests, so we will
|
||||
// create him at the top.
|
||||
people.add_active_user(isaac);
|
||||
|
||||
// We are going to test a core module called messages_store.js next.
|
||||
// This is an example of a deep unit test, where our dependencies
|
||||
// are easy to test.
|
||||
|
||||
run_test("message_store", () => {
|
||||
message_store.clear_for_testing();
|
||||
stream_data.clear_subscriptions();
|
||||
stream_data.add_sub(denmark_stream);
|
||||
|
||||
const in_message = {...messages.isaac_to_denmark_stream};
|
||||
|
||||
assert.equal(in_message.alerted, undefined);
|
||||
message_store.set_message_booleans(in_message);
|
||||
assert.equal(in_message.alerted, true);
|
||||
|
||||
// Let's add a message into our message_store via
|
||||
// add_message_metadata.
|
||||
assert.equal(message_store.get(in_message.id), undefined);
|
||||
message_store.add_message_metadata(in_message);
|
||||
const message = message_store.get(in_message.id);
|
||||
assert.equal(message, in_message);
|
||||
|
||||
// There are more side effects.
|
||||
const topic_names = stream_topic_history.get_recent_topic_names(denmark_stream.stream_id);
|
||||
assert.deepEqual(topic_names, ["copenhagen"]);
|
||||
});
|
||||
|
||||
// Tracking unread messages is a very fundamental part of the Zulip
|
||||
// app, and we use the unread object to track unread messages.
|
||||
|
||||
run_test("unread", () => {
|
||||
unread.declare_bankruptcy();
|
||||
stream_data.clear_subscriptions();
|
||||
stream_data.add_sub(denmark_stream);
|
||||
|
||||
const stream_id = denmark_stream.stream_id;
|
||||
const topic_name = "copenhagen";
|
||||
|
||||
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 0);
|
||||
|
||||
const in_message = {...messages.isaac_to_denmark_stream};
|
||||
message_store.set_message_booleans(in_message);
|
||||
|
||||
unread.process_loaded_messages([in_message]);
|
||||
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 1);
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {set_global, zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
// In the Zulip app you can narrow your message stream by topic, by
|
||||
// sender, by PM recipient, by search keywords, etc. We will discuss
|
||||
// narrows more broadly, but first let's test out a core piece of
|
||||
// code that makes things work.
|
||||
|
||||
const {Filter} = zrequire("../js/filter");
|
||||
const stream_data = zrequire("stream_data");
|
||||
|
||||
// This is the first time we have to deal with page_params. We
|
||||
// use set_global to set it to our preferred state.
|
||||
// (There are a few global variables in the Zulip codebase, and
|
||||
// page_params has a lot of important data shared by various
|
||||
// modules. Most of the data is irrelevant to out tests.)
|
||||
set_global("page_params", {
|
||||
// Use this to explicitly say we are not a special Zephyr
|
||||
// realm, since we want to test the "normal" codepath.
|
||||
realm_is_zephyr_mirror_realm: false,
|
||||
});
|
||||
|
||||
const denmark_stream = {
|
||||
color: "blue",
|
||||
name: "Denmark",
|
||||
stream_id: 101,
|
||||
subscribed: false,
|
||||
};
|
||||
|
||||
run_test("filter", () => {
|
||||
stream_data.clear_subscriptions();
|
||||
stream_data.add_sub(denmark_stream);
|
||||
|
||||
const filter_terms = [
|
||||
{operator: "stream", operand: "Denmark"},
|
||||
{operator: "topic", operand: "copenhagen"},
|
||||
];
|
||||
|
||||
const filter = new Filter(filter_terms);
|
||||
|
||||
const predicate = filter.predicate();
|
||||
|
||||
// We don't need full-fledged messages to test the gist of
|
||||
// our filter. If there are details that are distracting from
|
||||
// your test, you should not feel guilty about removing them.
|
||||
assert.equal(predicate({type: "personal"}), false);
|
||||
|
||||
assert.equal(
|
||||
predicate({
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "does not match filter",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
predicate({
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "copenhagen",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// We have a "narrow" abstraction that sits roughly on top of the
|
||||
// "filter" abstraction. If you are in a narrow, we track the
|
||||
// state with the narrow_state module.
|
||||
|
||||
const narrow_state = zrequire("narrow_state");
|
||||
|
||||
run_test("narrow_state", () => {
|
||||
stream_data.clear_subscriptions();
|
||||
stream_data.add_sub(denmark_stream);
|
||||
narrow_state.reset_current_filter();
|
||||
|
||||
// As we often do, first make assertions about the starting
|
||||
// state:
|
||||
|
||||
assert.equal(narrow_state.stream(), undefined);
|
||||
|
||||
// Now set up a Filter object.
|
||||
const filter_terms = [
|
||||
{operator: "stream", operand: "Denmark"},
|
||||
{operator: "topic", operand: "copenhagen"},
|
||||
];
|
||||
|
||||
const filter = new Filter(filter_terms);
|
||||
|
||||
// And here is where we actually change state.
|
||||
narrow_state.set_current_filter(filter);
|
||||
|
||||
assert.equal(narrow_state.stream(), "Denmark");
|
||||
assert.equal(narrow_state.topic(), "copenhagen");
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {mock_esm, zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
/*
|
||||
|
||||
Let's step back and review what we've done so far.
|
||||
|
||||
We've used fairly straightforward testing techniques
|
||||
to explore the following modules:
|
||||
|
||||
filter
|
||||
message_store
|
||||
narrow_state
|
||||
people
|
||||
stream_data
|
||||
util
|
||||
|
||||
We haven't gone deep on any of these objects, but if
|
||||
you are interested, all of these objects have test
|
||||
suites that have 100% line coverage on the modules
|
||||
that implement those objects. For example, you can look
|
||||
at people.js in this directory for more tests on the
|
||||
people object.
|
||||
|
||||
We can quickly review some testing concepts:
|
||||
|
||||
zrequire - bring in real code
|
||||
set_global - create stubs
|
||||
assert.equal - verify results
|
||||
|
||||
------
|
||||
|
||||
Let's talk about our next steps.
|
||||
|
||||
An app is pretty useless without an actual data source.
|
||||
One of the primary ways that a Zulip client gets data
|
||||
is through events. (We also get data at page load, and
|
||||
we can also ask the server for data, but that's not in
|
||||
the scope of this conversation yet.)
|
||||
|
||||
Chat systems are dynamic. If an admin adds a user, or
|
||||
if a user sends a messages, the server immediately sends
|
||||
events to all clients so that they can reflect appropriate
|
||||
changes in their UI. We're not going to discuss the entire
|
||||
"full stack" mechanism here. Instead, we'll focus on
|
||||
the client code, starting at the boundary where we
|
||||
process events.
|
||||
|
||||
Let's just get started...
|
||||
|
||||
*/
|
||||
|
||||
// We are going to use mock versions of some of our libraries.
|
||||
const activity = mock_esm("../../static/js/activity");
|
||||
const message_live_update = mock_esm("../../static/js/message_live_update");
|
||||
const pm_list = mock_esm("../../static/js/pm_list");
|
||||
const settings_users = mock_esm("../../static/js/settings_users");
|
||||
|
||||
// Use real versions of these modules.
|
||||
const people = zrequire("people");
|
||||
const server_events_dispatch = zrequire("server_events_dispatch");
|
||||
|
||||
const bob = {
|
||||
email: "bob@example.com",
|
||||
user_id: 33,
|
||||
full_name: "Bob Roberts",
|
||||
};
|
||||
|
||||
run_test("add users with event", () => {
|
||||
people.init();
|
||||
|
||||
const event = {
|
||||
type: "realm_user",
|
||||
op: "add",
|
||||
person: bob,
|
||||
};
|
||||
|
||||
assert(!people.is_known_user_id(bob.user_id));
|
||||
|
||||
// Let's simulate dispatching our event!
|
||||
server_events_dispatch.dispatch_normal_event(event);
|
||||
|
||||
// And it works!
|
||||
assert(people.is_known_user_id(bob.user_id));
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
It's actually a little surprising that adding a user does
|
||||
not have side effects beyond the people object. I guess
|
||||
we don't immediately update the buddy list, but that's
|
||||
because the buddy list gets updated on the next server
|
||||
fetch.
|
||||
|
||||
Let's try an update next. To make this work, we will want
|
||||
to override some of our stubs.
|
||||
|
||||
This is where we see a little extra benefit from the
|
||||
run_test wrapper. It passes us in an object that we
|
||||
can use to override data, and that works within the
|
||||
scope of the function.
|
||||
|
||||
*/
|
||||
|
||||
run_test("update user with event", (override) => {
|
||||
people.init();
|
||||
people.add_active_user(bob);
|
||||
|
||||
const new_bob = {
|
||||
email: "bob@example.com",
|
||||
user_id: bob.user_id,
|
||||
full_name: "The Artist Formerly Known as Bob",
|
||||
};
|
||||
|
||||
const event = {
|
||||
type: "realm_user",
|
||||
op: "update",
|
||||
person: new_bob,
|
||||
};
|
||||
|
||||
// We have to stub a few things. We don't want to test
|
||||
// the details of these functions, but we do want to
|
||||
// verify that they run. Fortunately, the run_test()
|
||||
// wrapper will tell us if we override a method that
|
||||
// doesn't get called!
|
||||
override(activity, "redraw", () => {});
|
||||
override(message_live_update, "update_user_full_name", () => {});
|
||||
override(pm_list, "update_private_messages", () => {});
|
||||
override(settings_users, "update_user_data", () => {});
|
||||
|
||||
// Dispatch the realm_user/update event, which will update
|
||||
// data structures and have other side effects that are
|
||||
// stubbed out above.
|
||||
server_events_dispatch.dispatch_normal_event(event);
|
||||
|
||||
const user = people.get_by_user_id(bob.user_id);
|
||||
|
||||
// Verify that the code actually did its main job:
|
||||
assert.equal(user.full_name, "The Artist Formerly Known as Bob");
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
/*
|
||||
Our test from an earlier example verifies that the update events
|
||||
leads to a name change inside the people object, but it obviously
|
||||
kind of glosses over the other interactions.
|
||||
|
||||
We can go a step further and verify the sequence of operations that
|
||||
happen during an event. This concept is called "stubbing", and you
|
||||
can find libraries to help do stubbing. Here we will just build our
|
||||
own lightweight stubbing system, which is almost trivially easy to
|
||||
do in a language like JavaScript.
|
||||
|
||||
*/
|
||||
|
||||
// First we tell the compiler to skip certain modules and just
|
||||
// replace them with {}.
|
||||
const huddle_data = mock_esm("../../static/js/huddle_data");
|
||||
const message_util = mock_esm("../../static/js/message_util");
|
||||
const notifications = mock_esm("../../static/js/notifications");
|
||||
const pm_list = mock_esm("../../static/js/pm_list");
|
||||
const resize = mock_esm("../../static/js/resize");
|
||||
const stream_list = mock_esm("../../static/js/stream_list");
|
||||
const unread_ops = mock_esm("../../static/js/unread_ops");
|
||||
const unread_ui = mock_esm("../../static/js/unread_ui");
|
||||
|
||||
set_global("home_msg_list", {});
|
||||
|
||||
// And we will also test some real code, of course.
|
||||
const message_events = zrequire("message_events");
|
||||
const message_store = zrequire("message_store");
|
||||
const narrow_state = zrequire("narrow_state");
|
||||
const people = zrequire("people");
|
||||
const recent_topics = zrequire("recent_topics");
|
||||
|
||||
const isaac = {
|
||||
email: "isaac@example.com",
|
||||
user_id: 30,
|
||||
full_name: "Isaac Newton",
|
||||
};
|
||||
people.add_active_user(isaac);
|
||||
|
||||
/*
|
||||
Next we create a test_helper that will allow us to redirect methods to an
|
||||
events array, and we can then later verify that the sequence of side effect
|
||||
is as predicted.
|
||||
|
||||
(Note that for now we don't simulate return values nor do we inspect the
|
||||
arguments to these functions. We could easily extend our helper to do more.)
|
||||
|
||||
The forthcoming example is a pretty extreme example, where we are calling a
|
||||
pretty high level method that dispatches a lot of its work out to other
|
||||
objects.
|
||||
|
||||
*/
|
||||
|
||||
function test_helper(override) {
|
||||
const events = [];
|
||||
|
||||
return {
|
||||
redirect: (module, func_name) => {
|
||||
override(module, func_name, () => {
|
||||
events.push([module, func_name]);
|
||||
});
|
||||
},
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
run_test("insert_message", (override) => {
|
||||
message_store.clear_for_testing();
|
||||
|
||||
override(pm_list, "update_private_messages", () => {});
|
||||
override(recent_topics, "is_visible", () => false);
|
||||
|
||||
const helper = test_helper(override);
|
||||
|
||||
const new_message = {
|
||||
sender_id: isaac.user_id,
|
||||
id: 1001,
|
||||
content: "example content",
|
||||
};
|
||||
|
||||
assert.equal(message_store.get(new_message.id), undefined);
|
||||
|
||||
helper.redirect(huddle_data, "process_loaded_messages");
|
||||
helper.redirect(message_util, "add_new_messages");
|
||||
helper.redirect(notifications, "received_messages");
|
||||
helper.redirect(resize, "resize_page_components");
|
||||
helper.redirect(stream_list, "update_streams_sidebar");
|
||||
helper.redirect(unread_ops, "process_visible");
|
||||
helper.redirect(unread_ui, "update_unread_counts");
|
||||
|
||||
narrow_state.reset_current_filter();
|
||||
|
||||
message_events.insert_new_messages([new_message]);
|
||||
|
||||
// Even though we have stubbed a *lot* of code, our
|
||||
// tests can still verify the main "narrative" of how
|
||||
// the code invokes various objects when a new message
|
||||
// comes in:
|
||||
assert.deepEqual(helper.events, [
|
||||
[huddle_data, "process_loaded_messages"],
|
||||
[message_util, "add_new_messages"],
|
||||
[message_util, "add_new_messages"],
|
||||
[unread_ui, "update_unread_counts"],
|
||||
[resize, "resize_page_components"],
|
||||
[unread_ops, "process_visible"],
|
||||
[notifications, "received_messages"],
|
||||
[stream_list, "update_streams_sidebar"],
|
||||
]);
|
||||
|
||||
// Despite all of our stubbing/mocking, the call to
|
||||
// insert_new_messages will have created a very important
|
||||
// side effect that we can verify:
|
||||
const inserted_message = message_store.get(new_message.id);
|
||||
assert.equal(inserted_message.id, new_message.id);
|
||||
assert.equal(inserted_message.content, "example content");
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {make_stub} = require("../zjsunit/stub");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
/*
|
||||
The previous example was a bit extreme. Generally we just
|
||||
use the make_stub helper that comes with zjsunit.
|
||||
|
||||
We will step away from the actual Zulip codebase for a
|
||||
second and just explore a contrived example.
|
||||
*/
|
||||
|
||||
run_test("explore make_stub", (override) => {
|
||||
// Let's say you have to test the following code.
|
||||
|
||||
const app = {
|
||||
notify_server_of_deposit(deposit_amount) {
|
||||
// simulate difficulty
|
||||
throw new Error(`We cannot report this value without wifi: ${deposit_amount}`);
|
||||
},
|
||||
|
||||
pop_up_fancy_confirmation_screen(deposit_amount, label) {
|
||||
// simulate difficulty
|
||||
throw new Error(`We cannot make a ${label} dialog for amount ${deposit_amount}`);
|
||||
},
|
||||
};
|
||||
|
||||
let balance = 40;
|
||||
|
||||
function deposit_paycheck(paycheck_amount) {
|
||||
balance += paycheck_amount;
|
||||
app.notify_server_of_deposit(paycheck_amount);
|
||||
app.pop_up_fancy_confirmation_screen(paycheck_amount, "paycheck");
|
||||
}
|
||||
|
||||
// Our deposit_paycheck should be easy to unit test for its
|
||||
// core functionality (updating your balance), but the side
|
||||
// effects get in the way. We have to override them to do
|
||||
// the simple test here.
|
||||
|
||||
override(app, "notify_server_of_deposit", () => {});
|
||||
override(app, "pop_up_fancy_confirmation_screen", () => {});
|
||||
deposit_paycheck(10);
|
||||
assert.equal(balance, 50);
|
||||
|
||||
// But we can do a little better here. Even though
|
||||
// the two side-effect functions are awkward here, we can
|
||||
// at least make sure they are invoked correctly. Let's
|
||||
// use stubs.
|
||||
|
||||
const notify_stub = make_stub();
|
||||
const pop_up_stub = make_stub();
|
||||
|
||||
// This time we'll just use our override helper to connect the
|
||||
// stubs.
|
||||
override(app, "notify_server_of_deposit", notify_stub.f);
|
||||
override(app, "pop_up_fancy_confirmation_screen", pop_up_stub.f);
|
||||
|
||||
deposit_paycheck(25);
|
||||
assert.equal(balance, 75);
|
||||
|
||||
assert.deepEqual(notify_stub.get_args("amount"), {amount: 25});
|
||||
assert.deepEqual(pop_up_stub.get_args("amount", "label"), {amount: 25, label: "paycheck"});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
"use strict";
|
||||
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
|
||||
/*
|
||||
|
||||
Let's continue to explore how we can test complex
|
||||
interactions in the real code.
|
||||
|
||||
When a new message comes in, we update the three major
|
||||
panes of the app:
|
||||
|
||||
* left sidebar - stream list
|
||||
* middle pane - message view
|
||||
* right sidebar - buddy list (aka "activity" list)
|
||||
|
||||
These are reflected by the following calls:
|
||||
|
||||
stream_list.update_streams_sidebar
|
||||
message_util.add_new_messages
|
||||
activity.process_loaded_messages
|
||||
|
||||
For now, though, let's focus on another side effect
|
||||
of processing incoming messages:
|
||||
|
||||
unread_ops.process_visible
|
||||
|
||||
When new messages come in, they are often immediately
|
||||
visible to users, so the app will communicate this
|
||||
back to the server by calling unread_ops.process_visible.
|
||||
|
||||
In order to unit test this, we don't want to require
|
||||
an actual server to be running. Instead, this example
|
||||
will stub many of the "boundaries" to focus on the
|
||||
core behavior.
|
||||
|
||||
The two key pieces here are as follows:
|
||||
|
||||
* Use mock_esm to avoid compiling the "real"
|
||||
modules that are immaterial to our current
|
||||
testing concerns.
|
||||
|
||||
* Use override(...) to simulate how we want
|
||||
methods to behave. (Often we want them to
|
||||
do nothing at all or return a simple
|
||||
value.)
|
||||
*/
|
||||
|
||||
const channel = mock_esm("../../static/js/channel");
|
||||
const message_list = mock_esm("../../static/js/message_list");
|
||||
const message_viewport = mock_esm("../../static/js/message_viewport");
|
||||
const notifications = mock_esm("../../static/js/notifications");
|
||||
const overlays = mock_esm("../../static/js/overlays");
|
||||
const unread_ui = mock_esm("../../static/js/unread_ui");
|
||||
|
||||
const message_store = zrequire("message_store");
|
||||
const recent_topics = zrequire("recent_topics");
|
||||
const stream_data = zrequire("stream_data");
|
||||
const unread = zrequire("unread");
|
||||
const unread_ops = zrequire("unread_ops");
|
||||
|
||||
const current_msg_list = set_global("current_msg_list", {});
|
||||
const home_msg_list = set_global("home_msg_list", {});
|
||||
|
||||
const denmark_stream = {
|
||||
color: "blue",
|
||||
name: "Denmark",
|
||||
stream_id: 101,
|
||||
subscribed: false,
|
||||
};
|
||||
|
||||
run_test("unread_ops", (override) => {
|
||||
stream_data.clear_subscriptions();
|
||||
stream_data.add_sub(denmark_stream);
|
||||
message_store.clear_for_testing();
|
||||
unread.declare_bankruptcy();
|
||||
|
||||
const message_id = 50;
|
||||
const test_messages = [
|
||||
{
|
||||
id: message_id,
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "copenhagen",
|
||||
unread: true,
|
||||
},
|
||||
];
|
||||
|
||||
override(recent_topics, "is_visible", () => false);
|
||||
|
||||
// Make our test message appear to be unread, so that
|
||||
// we then need to subsequently process them as read.
|
||||
unread.process_loaded_messages(test_messages);
|
||||
|
||||
// Make our window appear visible.
|
||||
override(notifications, "is_window_focused", () => true);
|
||||
|
||||
// Make our "test" message appear visible.
|
||||
override(message_viewport, "bottom_message_visible", () => true);
|
||||
|
||||
// Make us not be in a narrow (somewhat hackily).
|
||||
message_list.narrowed = undefined;
|
||||
|
||||
// Set current_msg_list containing messages that can be marked read
|
||||
override(current_msg_list, "all_messages", () => test_messages);
|
||||
|
||||
// Ignore these interactions for now:
|
||||
message_list.all = {
|
||||
show_message_as_read() {},
|
||||
};
|
||||
override(home_msg_list, "show_message_as_read", () => {});
|
||||
override(notifications, "close_notification", () => {});
|
||||
override(unread_ui, "update_unread_counts", () => {});
|
||||
|
||||
// Set up a way to capture the options passed in to channel.post.
|
||||
let channel_post_opts;
|
||||
override(channel, "post", (opts) => {
|
||||
channel_post_opts = opts;
|
||||
});
|
||||
|
||||
// Let the real code skip over details related to active overlays.
|
||||
override(overlays, "is_active", () => false);
|
||||
|
||||
let can_mark_messages_read;
|
||||
|
||||
// Set up an override to point to the above var, so we can
|
||||
// toggle it easily from within the test (and avoid complicated
|
||||
// data setup).
|
||||
override(current_msg_list, "can_mark_messages_read", () => can_mark_messages_read);
|
||||
|
||||
// First, test for a message list that cannot read messages.
|
||||
can_mark_messages_read = false;
|
||||
unread_ops.process_visible();
|
||||
|
||||
assert.deepEqual(channel_post_opts, undefined);
|
||||
|
||||
// Now flip the boolean, and get to the main thing we are testing.
|
||||
can_mark_messages_read = true;
|
||||
unread_ops.process_visible();
|
||||
|
||||
// The most important side effect of the above call is that
|
||||
// we post info to the server. We can verify that the correct
|
||||
// url and parameters are specified:
|
||||
assert.deepEqual(channel_post_opts, {
|
||||
url: "/json/messages/flags",
|
||||
idempotent: true,
|
||||
data: {messages: "[50]", op: "add", flag: "read"},
|
||||
success: channel_post_opts.success,
|
||||
});
|
||||
|
||||
// Simulate a successful post (which also clears the queue
|
||||
// in our message_flag code).
|
||||
channel_post_opts.success({messages: [message_id]});
|
||||
});
|
|
@ -1,790 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// This is a general tour of how to write node tests that
|
||||
// may also give you some quick insight on how the Zulip
|
||||
// browser app is constructed.
|
||||
|
||||
// The statements below are pretty typical for most node
|
||||
// tests. The reason we need these helpers will hopefully
|
||||
// become clear as you keep reading.
|
||||
const {strict: assert} = require("assert");
|
||||
|
||||
const {
|
||||
mock_cjs,
|
||||
mock_esm,
|
||||
set_global,
|
||||
unmock_module,
|
||||
with_field,
|
||||
zrequire,
|
||||
} = require("../zjsunit/namespace");
|
||||
const {make_stub} = require("../zjsunit/stub");
|
||||
const {run_test} = require("../zjsunit/test");
|
||||
const $ = require("../zjsunit/zjquery");
|
||||
|
||||
// Some quick housekeeping: Let's clear page_params, which is a data
|
||||
// structure that the server sends down to us when the app starts. We
|
||||
// prefer to test with a clean slate.
|
||||
|
||||
mock_cjs("jquery", $);
|
||||
const activity = mock_esm("../../static/js/activity");
|
||||
const channel = mock_esm("../../static/js/channel");
|
||||
const home_msg_list = set_global("home_msg_list", {});
|
||||
const message_list = mock_esm("../../static/js/message_list");
|
||||
const message_live_update = mock_esm("../../static/js/message_live_update");
|
||||
const message_util = mock_esm("../../static/js/message_util");
|
||||
const message_viewport = mock_esm("../../static/js/message_viewport");
|
||||
const notifications = mock_esm("../../static/js/notifications");
|
||||
const overlays = mock_esm("../../static/js/overlays");
|
||||
const pm_list = mock_esm("../../static/js/pm_list");
|
||||
const resize = mock_esm("../../static/js/resize");
|
||||
const settings_users = mock_esm("../../static/js/settings_users");
|
||||
const topic_list = mock_esm("../../static/js/topic_list");
|
||||
const unread_ui = mock_esm("../../static/js/unread_ui");
|
||||
|
||||
let stream_list = mock_esm("../../static/js/stream_list");
|
||||
let unread_ops = mock_esm("../../static/js/unread_ops");
|
||||
|
||||
set_global("page_params", {});
|
||||
|
||||
// Let's start with testing a function from util.js.
|
||||
|
||||
//
|
||||
// We will use our special zrequire helper to import the
|
||||
// code from util. We use zrequire instead of require,
|
||||
// because it has some magic to clear state when we move
|
||||
// on to the next test.
|
||||
//
|
||||
// The most basic unit tests load up code, call functions,
|
||||
// and assert truths:
|
||||
|
||||
const util = zrequire("util");
|
||||
assert(!util.find_wildcard_mentions("boring text"));
|
||||
assert(util.find_wildcard_mentions("mention @**everyone**"));
|
||||
|
||||
// Let's test with people.js next. We'll show this technique:
|
||||
// * get a false value
|
||||
// * change the data
|
||||
// * get a true value
|
||||
|
||||
const people = zrequire("people");
|
||||
const isaac = {
|
||||
email: "isaac@example.com",
|
||||
user_id: 30,
|
||||
full_name: "Isaac Newton",
|
||||
};
|
||||
|
||||
assert(!people.is_known_user_id(isaac.user_id));
|
||||
people.add_active_user(isaac);
|
||||
assert(people.is_known_user_id(isaac.user_id));
|
||||
|
||||
// The `people`object is a very fundamental object in the
|
||||
// Zulip app. You can learn a lot more about it by reading
|
||||
// the tests in people.js in the same directory as this file.
|
||||
// Let's create the current user, which some future tests will
|
||||
// require.
|
||||
|
||||
const me = {
|
||||
email: "me@example.com",
|
||||
user_id: 31,
|
||||
full_name: "Me Myself",
|
||||
};
|
||||
people.add_active_user(me);
|
||||
people.initialize_current_user(me.user_id);
|
||||
|
||||
// Let's look at stream_data next, and we will start by putting
|
||||
// some data at module scope (since it may be useful for future
|
||||
// tests):
|
||||
|
||||
const denmark_stream = {
|
||||
color: "blue",
|
||||
name: "Denmark",
|
||||
stream_id: 101,
|
||||
subscribed: false,
|
||||
};
|
||||
|
||||
// We use both set_global and zrequire here for test isolation.
|
||||
//
|
||||
// We also introduce the run_test helper, which mostly just causes
|
||||
// a line of output to go to the console. It does a little more than
|
||||
// that, which we will see later.
|
||||
|
||||
const stream_data = zrequire("stream_data");
|
||||
|
||||
run_test("verify stream_data persists stream color", () => {
|
||||
assert.equal(stream_data.get_sub_by_name("Denmark"), undefined);
|
||||
stream_data.add_sub(denmark_stream);
|
||||
const sub = stream_data.get_sub_by_name("Denmark");
|
||||
assert.equal(sub.color, "blue");
|
||||
});
|
||||
|
||||
// Hopefully the basic patterns for testing data-oriented modules
|
||||
// are starting to become apparent. To reinforce that, we will present
|
||||
// few more examples that also expose you to some of our core
|
||||
// data objects. Also, we start testing some objects that have
|
||||
// deeper dependencies.
|
||||
|
||||
const messages = {
|
||||
isaac_to_denmark_stream: {
|
||||
id: 400,
|
||||
sender_id: isaac.user_id,
|
||||
stream_id: denmark_stream.stream_id,
|
||||
type: "stream",
|
||||
flags: ["has_alert_word"],
|
||||
topic: "copenhagen",
|
||||
// note we don't have every field that a "real" message
|
||||
// would have, and that can be fine
|
||||
},
|
||||
};
|
||||
|
||||
// We are going to test a core module called messages_store.js next.
|
||||
// This is an example of a deep unit test, where our dependencies
|
||||
// are easy to test. Start by requiring the dependencies:
|
||||
const unread = zrequire("unread");
|
||||
const stream_topic_history = zrequire("stream_topic_history");
|
||||
const recent_topics = zrequire("recent_topics");
|
||||
|
||||
// And finally require the module that we will test directly:
|
||||
const message_store = zrequire("message_store");
|
||||
|
||||
run_test("message_store", () => {
|
||||
const in_message = {...messages.isaac_to_denmark_stream};
|
||||
|
||||
assert.equal(in_message.alerted, undefined);
|
||||
message_store.set_message_booleans(in_message);
|
||||
assert.equal(in_message.alerted, true);
|
||||
|
||||
// Let's add a message into our message_store via
|
||||
// add_message_metadata.
|
||||
assert.equal(message_store.get(in_message.id), undefined);
|
||||
message_store.add_message_metadata(in_message);
|
||||
const message = message_store.get(in_message.id);
|
||||
assert.equal(message, in_message);
|
||||
|
||||
// There are more side effects.
|
||||
const topic_names = stream_topic_history.get_recent_topic_names(denmark_stream.stream_id);
|
||||
assert.deepEqual(topic_names, ["copenhagen"]);
|
||||
});
|
||||
|
||||
// Tracking unread messages is a very fundamental part of the Zulip
|
||||
// app, and we use the unread object to track unread messages.
|
||||
|
||||
run_test("unread", () => {
|
||||
const stream_id = denmark_stream.stream_id;
|
||||
const topic_name = "copenhagen";
|
||||
|
||||
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 0);
|
||||
|
||||
const in_message = {...messages.isaac_to_denmark_stream};
|
||||
message_store.set_message_booleans(in_message);
|
||||
|
||||
unread.process_loaded_messages([in_message]);
|
||||
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 1);
|
||||
});
|
||||
|
||||
// In the Zulip app you can narrow your message stream by topic, by
|
||||
// sender, by PM recipient, by search keywords, etc. We will discuss
|
||||
// narrows more broadly, but first let's test out a core piece of
|
||||
// code that makes things work.
|
||||
|
||||
// We use the second argument of zrequire to find the location of the
|
||||
// Filter class.
|
||||
const {Filter} = zrequire("../js/filter");
|
||||
|
||||
run_test("filter", () => {
|
||||
const filter_terms = [
|
||||
{operator: "stream", operand: "Denmark"},
|
||||
{operator: "topic", operand: "copenhagen"},
|
||||
];
|
||||
|
||||
const filter = new Filter(filter_terms);
|
||||
|
||||
const predicate = filter.predicate();
|
||||
|
||||
// We don't need full-fledged messages to test the gist of
|
||||
// our filter. If there are details that are distracting from
|
||||
// your test, you should not feel guilty about removing them.
|
||||
assert.equal(predicate({type: "personal"}), false);
|
||||
|
||||
assert.equal(
|
||||
predicate({
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "does not match filter",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
predicate({
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "copenhagen",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// We have a "narrow" abstraction that sits roughly on top of the
|
||||
// "filter" abstraction. If you are in a narrow, we track the
|
||||
// state with the narrow_state module.
|
||||
|
||||
const narrow_state = zrequire("narrow_state");
|
||||
|
||||
run_test("narrow_state", () => {
|
||||
// As we often do, first make assertions about the starting
|
||||
// state:
|
||||
|
||||
assert.equal(narrow_state.stream(), undefined);
|
||||
|
||||
// Now set the state.
|
||||
const filter_terms = [
|
||||
{operator: "stream", operand: "Denmark"},
|
||||
{operator: "topic", operand: "copenhagen"},
|
||||
];
|
||||
|
||||
const filter = new Filter(filter_terms);
|
||||
|
||||
narrow_state.set_current_filter(filter);
|
||||
|
||||
assert.equal(narrow_state.stream(), "Denmark");
|
||||
assert.equal(narrow_state.topic(), "copenhagen");
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Let's step back and review what we've done so far.
|
||||
|
||||
We've used fairly straightforward testing techniques
|
||||
to explore the following modules:
|
||||
|
||||
filter
|
||||
message_store
|
||||
narrow_state
|
||||
people
|
||||
stream_data
|
||||
util
|
||||
|
||||
We haven't gone deep on any of these objects, but if
|
||||
you are interested, all of these objects have test
|
||||
suites that have 100% line coverage on the modules
|
||||
that implement those objects. For example, you can look
|
||||
at people.js in this directory for more tests on the
|
||||
people object.
|
||||
|
||||
We can quickly review some testing concepts:
|
||||
|
||||
zrequire - bring in real code
|
||||
set_global - create stubs
|
||||
assert.equal - verify results
|
||||
|
||||
------
|
||||
|
||||
It's time to elaborate a bit on set_global.
|
||||
|
||||
First, some context. When we test certain objects,
|
||||
we don't always want to test all the code they
|
||||
depend on. Often we want to completely ignore the
|
||||
interactions with certain objects; other times, we
|
||||
will want to simulate some behavior of the objects
|
||||
we depend on without bringing in all the implementation
|
||||
details.
|
||||
|
||||
Also, our test runner runs many tests back to back.
|
||||
Between each test we need to essentially reset the global
|
||||
object back to its original state, so that state doesn't
|
||||
leak between tests.
|
||||
|
||||
That's where set_global comes in. When you call
|
||||
set_global, it updates the global namespace with an
|
||||
object that you specify in the **test**, not real
|
||||
code. Using set_global explicitly tells your test
|
||||
reader what your testing boundaries are between "real"
|
||||
code and "simulated" code. Finally, and perhaps most
|
||||
importantly, the test runner will prevent this state
|
||||
from leaking into the next test (and "zrequire" has
|
||||
the same behavior attached to it as well).
|
||||
|
||||
------
|
||||
|
||||
Let's talk about our next steps.
|
||||
|
||||
An app is pretty useless without an actual data source.
|
||||
One of the primary ways that a Zulip client gets data
|
||||
is through events. (We also get data at page load, and
|
||||
we can also ask the server for data, but that's not in
|
||||
the scope of this conversation yet.)
|
||||
|
||||
Chat systems are dynamic. If an admin adds a user, or
|
||||
if a user sends a messages, the server immediately sends
|
||||
events to all clients so that they can reflect appropriate
|
||||
changes in their UI. We're not going to discuss the entire
|
||||
"full stack" mechanism here. Instead, we'll focus on
|
||||
the client code, starting at the boundary where we
|
||||
process events.
|
||||
|
||||
Let's just get started...
|
||||
|
||||
*/
|
||||
|
||||
const server_events_dispatch = zrequire("server_events_dispatch");
|
||||
|
||||
// We will use Bob in several tests.
|
||||
const bob = {
|
||||
email: "bob@example.com",
|
||||
user_id: 33,
|
||||
full_name: "Bob Roberts",
|
||||
};
|
||||
|
||||
run_test("add_user_event", () => {
|
||||
const event = {
|
||||
type: "realm_user",
|
||||
op: "add",
|
||||
person: bob,
|
||||
};
|
||||
|
||||
assert(!people.is_known_user_id(bob.user_id));
|
||||
server_events_dispatch.dispatch_normal_event(event);
|
||||
assert(people.is_known_user_id(bob.user_id));
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
It's actually a little surprising that adding a user does
|
||||
not have side effects beyond the people object. I guess
|
||||
we don't immediately update the buddy list, but that's
|
||||
because the buddy list gets updated on the next server
|
||||
fetch.
|
||||
|
||||
Let's try an update next. To make this work, we will want
|
||||
to put some stub objects into the global namespace (as
|
||||
opposed to using the "real" code).
|
||||
|
||||
This is where we see a little extra benefit from the
|
||||
run_test wrapper. It passes us in an object that we
|
||||
can use to override data, and that works within the
|
||||
scope of the function.
|
||||
|
||||
*/
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
run_test("update_user_event", (override) => {
|
||||
const new_bob = {
|
||||
email: "bob@example.com",
|
||||
user_id: bob.user_id,
|
||||
full_name: "The Artist Formerly Known as Bob",
|
||||
};
|
||||
|
||||
const event = {
|
||||
type: "realm_user",
|
||||
op: "update",
|
||||
person: new_bob,
|
||||
};
|
||||
|
||||
// We have to stub a few things:
|
||||
override(activity, "redraw", noop);
|
||||
override(message_live_update, "update_user_full_name", noop);
|
||||
override(pm_list, "update_private_messages", noop);
|
||||
override(settings_users, "update_user_data", noop);
|
||||
|
||||
// Dispatch the realm_user/update event, which will update
|
||||
// data structures and have other side effects that are
|
||||
// stubbed out above.
|
||||
server_events_dispatch.dispatch_normal_event(event);
|
||||
|
||||
const user = people.get_by_user_id(bob.user_id);
|
||||
|
||||
// Verify that the code actually did its main job:
|
||||
assert.equal(user.full_name, "The Artist Formerly Known as Bob");
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Our test verifies that the update events leads to a name change
|
||||
inside the people object, but it obviously kind of glosses over
|
||||
the other interactions.
|
||||
|
||||
We can go a step further and verify the sequence of operations
|
||||
that happen during an event. This concept is called "mocking",
|
||||
and you can find libraries to help do mocking. Here we will
|
||||
just build our own lightweight mocking system, which is almost
|
||||
trivially easy to do in a language like JavaScript.
|
||||
|
||||
*/
|
||||
|
||||
function test_helper() {
|
||||
const events = [];
|
||||
|
||||
return {
|
||||
redirect: (module, func_name) => {
|
||||
module[func_name] = () => {
|
||||
events.push([module, func_name]);
|
||||
};
|
||||
},
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Our test_helper will allow us to redirect methods to an
|
||||
events array, and we can then later verify that the sequence
|
||||
of side effect is as predicted.
|
||||
|
||||
(Note that for now we don't simulate return values nor do we
|
||||
inspect the arguments to these functions. We could easily
|
||||
extend our helper to do more.)
|
||||
|
||||
The forthcoming example is a pretty extreme example, where we
|
||||
are calling a pretty high level method that dispatches
|
||||
a lot of its work out to other objects.
|
||||
|
||||
*/
|
||||
|
||||
const huddle_data = zrequire("huddle_data");
|
||||
const message_events = zrequire("message_events");
|
||||
|
||||
run_test("insert_message", (override) => {
|
||||
override(pm_list, "update_private_messages", noop);
|
||||
|
||||
const helper = test_helper();
|
||||
recent_topics.is_visible = () => false;
|
||||
|
||||
const new_message = {
|
||||
sender_id: isaac.user_id,
|
||||
id: 1001,
|
||||
content: "example content",
|
||||
};
|
||||
|
||||
assert.equal(message_store.get(new_message.id), undefined);
|
||||
|
||||
helper.redirect(huddle_data, "process_loaded_messages");
|
||||
helper.redirect(message_util, "add_new_messages");
|
||||
helper.redirect(notifications, "received_messages");
|
||||
helper.redirect(resize, "resize_page_components");
|
||||
helper.redirect(stream_list, "update_streams_sidebar");
|
||||
helper.redirect(unread_ops, "process_visible");
|
||||
helper.redirect(unread_ui, "update_unread_counts");
|
||||
|
||||
narrow_state.reset_current_filter();
|
||||
|
||||
message_events.insert_new_messages([new_message]);
|
||||
|
||||
// Even though we have stubbed a *lot* of code, our
|
||||
// tests can still verify the main "narrative" of how
|
||||
// the code invokes various objects when a new message
|
||||
// comes in:
|
||||
assert.deepEqual(helper.events, [
|
||||
[huddle_data, "process_loaded_messages"],
|
||||
[message_util, "add_new_messages"],
|
||||
[message_util, "add_new_messages"],
|
||||
[unread_ui, "update_unread_counts"],
|
||||
[resize, "resize_page_components"],
|
||||
[unread_ops, "process_visible"],
|
||||
[notifications, "received_messages"],
|
||||
[stream_list, "update_streams_sidebar"],
|
||||
]);
|
||||
|
||||
// Despite all of our stubbing/mocking, the call to
|
||||
// insert_new_messages will have created a very important
|
||||
// side effect that we can verify:
|
||||
const inserted_message = message_store.get(new_message.id);
|
||||
assert.equal(inserted_message.id, new_message.id);
|
||||
assert.equal(inserted_message.content, "example content");
|
||||
});
|
||||
|
||||
/*
|
||||
The above example is a bit extreme. Generally we just
|
||||
use the make_stub helper that comes with zjsunit.
|
||||
|
||||
We will step away from the actual Zulip codebase for a
|
||||
second and just explore a contrived example.
|
||||
*/
|
||||
|
||||
run_test("explore make_stub", (override) => {
|
||||
// Let's say you have to test the following code.
|
||||
|
||||
const app = {
|
||||
notify_server_of_deposit(deposit_amount) {
|
||||
// simulate difficulty
|
||||
throw new Error(`We cannot report this value without wifi: ${deposit_amount}`);
|
||||
},
|
||||
|
||||
pop_up_fancy_confirmation_screen(deposit_amount, label) {
|
||||
// simulate difficulty
|
||||
throw new Error(`We cannot make a ${label} dialog for amount ${deposit_amount}`);
|
||||
},
|
||||
};
|
||||
|
||||
let balance = 40;
|
||||
|
||||
function deposit_paycheck(paycheck_amount) {
|
||||
balance += paycheck_amount;
|
||||
app.notify_server_of_deposit(paycheck_amount);
|
||||
app.pop_up_fancy_confirmation_screen(paycheck_amount, "paycheck");
|
||||
}
|
||||
|
||||
// Our deposit_paycheck should be easy to unit test for its
|
||||
// core functionality (updating your balance), but the side
|
||||
// effects get in the way. We have to override them to do
|
||||
// the simple test here.
|
||||
|
||||
with_field(app, "notify_server_of_deposit", noop, () => {
|
||||
with_field(app, "pop_up_fancy_confirmation_screen", noop, () => {
|
||||
deposit_paycheck(10);
|
||||
});
|
||||
});
|
||||
assert.equal(balance, 50);
|
||||
|
||||
// But we can do a little better here. Even though
|
||||
// the two side-effect functions are awkward here, we can
|
||||
// at least make sure they are invoked correctly. Let's
|
||||
// use stubs.
|
||||
|
||||
const notify_stub = make_stub();
|
||||
const pop_up_stub = make_stub();
|
||||
|
||||
// This time we'll just use our override helper to connect the
|
||||
// stubs.
|
||||
override(app, "notify_server_of_deposit", notify_stub.f);
|
||||
override(app, "pop_up_fancy_confirmation_screen", pop_up_stub.f);
|
||||
|
||||
deposit_paycheck(25);
|
||||
assert.equal(balance, 75);
|
||||
|
||||
assert.deepEqual(notify_stub.get_args("amount"), {amount: 25});
|
||||
assert.deepEqual(pop_up_stub.get_args("amount", "label"), {amount: 25, label: "paycheck"});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Let's continue to explore how we can test complex
|
||||
interactions in the real code.
|
||||
|
||||
When a new message comes in, we update the three major
|
||||
panes of the app:
|
||||
|
||||
* left sidebar - stream list
|
||||
* middle pane - message view
|
||||
* right sidebar - buddy list (aka "activity" list)
|
||||
|
||||
These are reflected by the following calls:
|
||||
|
||||
stream_list.update_streams_sidebar
|
||||
message_util.add_new_messages
|
||||
activity.process_loaded_messages
|
||||
|
||||
For now, though, let's focus on another side effect
|
||||
of processing incoming messages:
|
||||
|
||||
unread_ops.process_visible
|
||||
|
||||
When new messages come in, they are often immediately
|
||||
visible to users, so the app will communicate this
|
||||
back to the server by calling unread_ops.process_visible.
|
||||
|
||||
In order to unit test this, we don't want to require
|
||||
an actual server to be running. Instead, this example
|
||||
will stub many of the "boundaries" to focus on the
|
||||
core behavior.
|
||||
|
||||
*/
|
||||
|
||||
unmock_module("../../static/js/unread_ops");
|
||||
unread_ops = zrequire("unread_ops");
|
||||
|
||||
run_test("unread_ops", (override) => {
|
||||
(function set_up() {
|
||||
const test_messages = [
|
||||
{
|
||||
id: 50,
|
||||
type: "stream",
|
||||
stream_id: denmark_stream.stream_id,
|
||||
topic: "copenhagen",
|
||||
unread: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Make our test message appear to be unread, so that
|
||||
// we then need to subsequently process them as read.
|
||||
unread.process_loaded_messages(test_messages);
|
||||
|
||||
// Make our window appear visible.
|
||||
override(notifications, "is_window_focused", () => true);
|
||||
|
||||
// Make our "test" message appear visible.
|
||||
override(message_viewport, "bottom_message_visible", () => true);
|
||||
|
||||
// Make us not be in a narrow (somewhat hackily).
|
||||
message_list.narrowed = undefined;
|
||||
|
||||
// Set current_message_list containing messages that
|
||||
// can be marked read
|
||||
set_global("current_msg_list", {
|
||||
all_messages: () => test_messages,
|
||||
can_mark_messages_read: () => true,
|
||||
});
|
||||
|
||||
// Ignore these interactions for now:
|
||||
message_list.all = {
|
||||
show_message_as_read() {},
|
||||
};
|
||||
override(home_msg_list, "show_message_as_read", noop);
|
||||
override(notifications, "close_notification", noop);
|
||||
})();
|
||||
|
||||
// Set up a way to capture the options passed in to channel.post.
|
||||
let channel_post_opts;
|
||||
override(channel, "post", (opts) => {
|
||||
channel_post_opts = opts;
|
||||
});
|
||||
|
||||
// Let the real code skip over details related to active overlays.
|
||||
override(overlays, "is_active", () => false);
|
||||
|
||||
// First, test for a message list that cannot read messages. Here
|
||||
// we use with_field to limit the scope of our stub function.
|
||||
with_field(
|
||||
current_msg_list,
|
||||
"can_mark_messages_read",
|
||||
() => false,
|
||||
() => {
|
||||
unread_ops.process_visible();
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(channel_post_opts, undefined);
|
||||
|
||||
with_field(
|
||||
current_msg_list,
|
||||
"can_mark_messages_read",
|
||||
() => true,
|
||||
() => {
|
||||
// Do the main thing we're testing!
|
||||
unread_ops.process_visible();
|
||||
},
|
||||
);
|
||||
|
||||
// The most important side effect of the above call is that
|
||||
// we post info to the server. We can verify that the correct
|
||||
// url and parameters are specified:
|
||||
assert.deepEqual(channel_post_opts, {
|
||||
url: "/json/messages/flags",
|
||||
idempotent: true,
|
||||
data: {messages: "[50]", op: "add", flag: "read"},
|
||||
success: channel_post_opts.success,
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Next we will explore this function:
|
||||
|
||||
stream_list.update_streams_sidebar
|
||||
|
||||
*/
|
||||
|
||||
unmock_module("../../static/js/stream_list");
|
||||
stream_list = zrequire("stream_list");
|
||||
|
||||
const social_stream = {
|
||||
color: "red",
|
||||
name: "Social",
|
||||
stream_id: 102,
|
||||
subscribed: true,
|
||||
};
|
||||
|
||||
function make_jquery_helper() {
|
||||
let appended_data;
|
||||
$("#stream_filters").append = (data) => {
|
||||
appended_data = data;
|
||||
};
|
||||
|
||||
return {
|
||||
verify_actions: () => {
|
||||
const expected_data_to_append = [["stream stub"]];
|
||||
|
||||
assert.deepEqual(appended_data, expected_data_to_append);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function make_topic_list_helper(override) {
|
||||
// We want to make sure that updating a stream_list
|
||||
// closes the topic list and then rebuilds it. We don't
|
||||
// care about the implementation details of topic_list for
|
||||
// now, just that it is invoked properly.
|
||||
override(topic_list, "active_stream_id", () => undefined);
|
||||
override(topic_list, "get_stream_li", () => undefined);
|
||||
|
||||
let topic_list_cleared;
|
||||
override(topic_list, "clear", () => {
|
||||
topic_list_cleared = true;
|
||||
});
|
||||
|
||||
let topic_list_closed;
|
||||
override(topic_list, "close", () => {
|
||||
topic_list_closed = true;
|
||||
});
|
||||
|
||||
let topic_list_rebuilt;
|
||||
override(topic_list, "rebuild", () => {
|
||||
topic_list_rebuilt = true;
|
||||
});
|
||||
|
||||
return {
|
||||
verify_actions: () => {
|
||||
assert(topic_list_cleared);
|
||||
assert(topic_list_closed);
|
||||
assert(topic_list_rebuilt);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function make_sidebar_helper() {
|
||||
let updated_whether_active;
|
||||
|
||||
function row_widget() {
|
||||
return {
|
||||
update_whether_active: () => {
|
||||
updated_whether_active = true;
|
||||
},
|
||||
get_li: () => ["stream stub"],
|
||||
};
|
||||
}
|
||||
|
||||
stream_list.stream_sidebar.set_row(social_stream.stream_id, row_widget());
|
||||
|
||||
return {
|
||||
verify_actions: () => {
|
||||
assert(updated_whether_active);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
run_test("stream_list", (override) => {
|
||||
stream_data.add_sub(social_stream);
|
||||
|
||||
const filter_terms = [
|
||||
{operator: "stream", operand: "Social"},
|
||||
{operator: "topic", operand: "lunch"},
|
||||
];
|
||||
|
||||
const filter = new Filter(filter_terms);
|
||||
|
||||
override(narrow_state, "filter", () => filter);
|
||||
override(narrow_state, "active", () => true);
|
||||
|
||||
const jquery_helper = make_jquery_helper();
|
||||
const sidebar_helper = make_sidebar_helper();
|
||||
const topic_list_helper = make_topic_list_helper(override);
|
||||
|
||||
// This is what we are testing!
|
||||
with_field(stream_list, "stream_cursor", {redraw: noop}, () => {
|
||||
stream_list.update_streams_sidebar();
|
||||
});
|
||||
|
||||
jquery_helper.verify_actions();
|
||||
sidebar_helper.verify_actions();
|
||||
topic_list_helper.verify_actions();
|
||||
});
|
|
@ -93,25 +93,6 @@ exports.mock_esm = (request, obj = {}) => {
|
|||
return exports.mock_cjs(request, {...obj, __esModule: true});
|
||||
};
|
||||
|
||||
exports.unmock_module = (request) => {
|
||||
const filename = Module._resolveFilename(
|
||||
request,
|
||||
require.cache[callsites()[1].getFileName()],
|
||||
false,
|
||||
);
|
||||
|
||||
if (!module_mocks.has(filename)) {
|
||||
throw new Error(`Cannot unmock ${filename}, which was not mocked`);
|
||||
}
|
||||
|
||||
if (!used_module_mocks.has(filename)) {
|
||||
throw new Error(`You asked to mock ${filename} but we never saw it during compilation.`);
|
||||
}
|
||||
|
||||
module_mocks.delete(filename);
|
||||
used_module_mocks.delete(filename);
|
||||
};
|
||||
|
||||
exports.set_global = function (name, val) {
|
||||
if (val === null) {
|
||||
throw new Error(`
|
||||
|
|
Loading…
Reference in New Issue