diff --git a/docs/testing/testing-with-node.md b/docs/testing/testing-with-node.md index 8ac37ab5d4..51c657e10a 100644 --- a/docs/testing/testing-with-node.md +++ b/docs/testing/testing-with-node.md @@ -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 diff --git a/frontend_tests/node_tests/example1.js b/frontend_tests/node_tests/example1.js new file mode 100644 index 0000000000..72d82a4717 --- /dev/null +++ b/frontend_tests/node_tests/example1.js @@ -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. diff --git a/frontend_tests/node_tests/example2.js b/frontend_tests/node_tests/example2.js new file mode 100644 index 0000000000..691515cce8 --- /dev/null +++ b/frontend_tests/node_tests/example2.js @@ -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); +}); diff --git a/frontend_tests/node_tests/example3.js b/frontend_tests/node_tests/example3.js new file mode 100644 index 0000000000..97d851fe32 --- /dev/null +++ b/frontend_tests/node_tests/example3.js @@ -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"); +}); diff --git a/frontend_tests/node_tests/example4.js b/frontend_tests/node_tests/example4.js new file mode 100644 index 0000000000..dff91cf209 --- /dev/null +++ b/frontend_tests/node_tests/example4.js @@ -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"); +}); diff --git a/frontend_tests/node_tests/example5.js b/frontend_tests/node_tests/example5.js new file mode 100644 index 0000000000..f0e0cf6bce --- /dev/null +++ b/frontend_tests/node_tests/example5.js @@ -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"); +}); diff --git a/frontend_tests/node_tests/example6.js b/frontend_tests/node_tests/example6.js new file mode 100644 index 0000000000..daf05ccbfe --- /dev/null +++ b/frontend_tests/node_tests/example6.js @@ -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"}); +}); diff --git a/frontend_tests/node_tests/example7.js b/frontend_tests/node_tests/example7.js new file mode 100644 index 0000000000..e1de755d03 --- /dev/null +++ b/frontend_tests/node_tests/example7.js @@ -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]}); +}); diff --git a/frontend_tests/node_tests/tutorial.js b/frontend_tests/node_tests/tutorial.js deleted file mode 100644 index 4b3d24b00c..0000000000 --- a/frontend_tests/node_tests/tutorial.js +++ /dev/null @@ -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(); -}); diff --git a/frontend_tests/zjsunit/namespace.js b/frontend_tests/zjsunit/namespace.js index 0301182dc9..db5e75238a 100644 --- a/frontend_tests/zjsunit/namespace.js +++ b/frontend_tests/zjsunit/namespace.js @@ -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(`