mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			727 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			727 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// 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.  Let's start with testing
 | 
						|
// a function from util.js.
 | 
						|
//
 | 
						|
// 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
 | 
						|
 | 
						|
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,
 | 
						|
};
 | 
						|
 | 
						|
// 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.
 | 
						|
 | 
						|
set_global("page_params", {});
 | 
						|
 | 
						|
zrequire("stream_data");
 | 
						|
 | 
						|
run_test("stream_data", () => {
 | 
						|
    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.  It
 | 
						|
// depends on some code that we aren't really interested in testing,
 | 
						|
// so let's create some stub functions that do nothing.
 | 
						|
 | 
						|
const noop = () => undefined;
 | 
						|
 | 
						|
set_global("alert_words", {});
 | 
						|
 | 
						|
alert_words.process_message = noop;
 | 
						|
 | 
						|
// We can also bring in real code:
 | 
						|
zrequire("recent_senders");
 | 
						|
zrequire("unread");
 | 
						|
zrequire("stream_topic_history");
 | 
						|
zrequire("recent_topics");
 | 
						|
zrequire("overlays");
 | 
						|
 | 
						|
// And finally require the module that we will test directly:
 | 
						|
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.
 | 
						|
zrequire("Filter", "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.
 | 
						|
 | 
						|
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...
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
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).
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
set_global("activity", {});
 | 
						|
set_global("message_live_update", {});
 | 
						|
set_global("pm_list", {});
 | 
						|
set_global("settings_users", {});
 | 
						|
 | 
						|
zrequire("user_events");
 | 
						|
 | 
						|
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 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_name, func_name) => {
 | 
						|
            const full_name = module_name + "." + func_name;
 | 
						|
            global[module_name][func_name] = () => {
 | 
						|
                events.push(full_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.
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
set_global("home_msg_list", {});
 | 
						|
set_global("message_list", {});
 | 
						|
set_global("message_util", {});
 | 
						|
set_global("notifications", {});
 | 
						|
set_global("resize", {});
 | 
						|
set_global("stream_list", {});
 | 
						|
set_global("unread_ops", {});
 | 
						|
set_global("unread_ui", {});
 | 
						|
 | 
						|
zrequire("message_events");
 | 
						|
 | 
						|
run_test("insert_message", (override) => {
 | 
						|
    override("pm_list.update_private_messages", noop);
 | 
						|
 | 
						|
    const helper = test_helper();
 | 
						|
 | 
						|
    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 previous example starts to get us out of the data layer of
 | 
						|
   the app and into more interesting interactions.
 | 
						|
 | 
						|
   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.
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
set_global("channel", {});
 | 
						|
set_global("home_msg_list", {});
 | 
						|
set_global("message_list", {});
 | 
						|
set_global("message_viewport", {});
 | 
						|
zrequire("message_flags");
 | 
						|
 | 
						|
zrequire("unread_ops");
 | 
						|
 | 
						|
run_test("unread_ops", () => {
 | 
						|
    (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.
 | 
						|
        notifications.window_has_focus = () => true;
 | 
						|
 | 
						|
        // Make our "test" message appear visible.
 | 
						|
        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:
 | 
						|
        home_msg_list.show_message_as_read = noop;
 | 
						|
        message_list.all = {};
 | 
						|
        message_list.all.show_message_as_read = noop;
 | 
						|
        notifications.close_notification = noop;
 | 
						|
    })();
 | 
						|
 | 
						|
    // Set up a way to capture the options passed in to channel.post.
 | 
						|
    let channel_post_opts;
 | 
						|
    channel.post = (opts) => {
 | 
						|
        channel_post_opts = opts;
 | 
						|
    };
 | 
						|
 | 
						|
    // First, test for a message list that cannot read messages
 | 
						|
    current_msg_list.can_mark_messages_read = () => false;
 | 
						|
    unread_ops.process_visible();
 | 
						|
    assert.deepEqual(channel_post_opts, undefined);
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
    To make this test work, we will create a somewhat elaborate
 | 
						|
    function that fills in for jQuery (https://jquery.com/), so that
 | 
						|
    one boundary of our tests is how stream_list.js calls into
 | 
						|
    stream_list to manipulate DOM.
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
set_global("topic_list", {});
 | 
						|
 | 
						|
zrequire("stream_sort");
 | 
						|
zrequire("stream_list");
 | 
						|
 | 
						|
const social_stream = {
 | 
						|
    color: "red",
 | 
						|
    name: "Social",
 | 
						|
    stream_id: 102,
 | 
						|
    subscribed: true,
 | 
						|
};
 | 
						|
 | 
						|
run_test("set_up_filter", () => {
 | 
						|
    stream_data.add_sub(social_stream);
 | 
						|
 | 
						|
    const filter_terms = [
 | 
						|
        {operator: "stream", operand: "Social"},
 | 
						|
        {operator: "topic", operand: "lunch"},
 | 
						|
    ];
 | 
						|
 | 
						|
    const filter = new Filter(filter_terms);
 | 
						|
 | 
						|
    narrow_state.filter = () => filter;
 | 
						|
    narrow_state.active = () => true;
 | 
						|
});
 | 
						|
 | 
						|
function jquery_elem() {
 | 
						|
    // We create basic stubs for jQuery elements, so they
 | 
						|
    // just work.  We can extend these in cases where we want
 | 
						|
    // more detailed testing.
 | 
						|
    const elem = {};
 | 
						|
 | 
						|
    elem.expectOne = () => elem;
 | 
						|
    elem.removeClass = () => elem;
 | 
						|
    elem.empty = () => elem;
 | 
						|
 | 
						|
    return elem;
 | 
						|
}
 | 
						|
 | 
						|
function make_jquery_helper() {
 | 
						|
    const stream_list_filter = jquery_elem();
 | 
						|
    stream_list_filter.val = () => "";
 | 
						|
 | 
						|
    const stream_filters = jquery_elem();
 | 
						|
 | 
						|
    let appended_data;
 | 
						|
    stream_filters.append = function (data) {
 | 
						|
        appended_data = data;
 | 
						|
    };
 | 
						|
 | 
						|
    function fake_jquery(selector) {
 | 
						|
        switch (selector) {
 | 
						|
            case ".stream-list-filter":
 | 
						|
                return stream_list_filter;
 | 
						|
            case "ul#stream_filters li":
 | 
						|
                return jquery_elem();
 | 
						|
            case "#stream_filters":
 | 
						|
                return stream_filters;
 | 
						|
            default:
 | 
						|
                throw Error("unknown selector: " + selector);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    set_global("$", fake_jquery);
 | 
						|
 | 
						|
    return {
 | 
						|
        verify_actions: () => {
 | 
						|
            const expected_data_to_append = [["stream stub"]];
 | 
						|
 | 
						|
            assert.deepEqual(appended_data, expected_data_to_append);
 | 
						|
        },
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function make_topic_list_helper() {
 | 
						|
    // 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.
 | 
						|
    topic_list.active_stream_id = () => undefined;
 | 
						|
    topic_list.get_stream_li = () => undefined;
 | 
						|
 | 
						|
    let topic_list_cleared;
 | 
						|
    topic_list.clear = () => {
 | 
						|
        topic_list_cleared = true;
 | 
						|
    };
 | 
						|
 | 
						|
    let topic_list_closed;
 | 
						|
    topic_list.close = () => {
 | 
						|
        topic_list_closed = true;
 | 
						|
    };
 | 
						|
 | 
						|
    let topic_list_rebuilt;
 | 
						|
    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());
 | 
						|
    stream_list.stream_cursor = {
 | 
						|
        redraw: noop,
 | 
						|
    };
 | 
						|
 | 
						|
    return {
 | 
						|
        verify_actions: () => {
 | 
						|
            assert(updated_whether_active);
 | 
						|
        },
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
zrequire("topic_zoom");
 | 
						|
 | 
						|
run_test("stream_list", () => {
 | 
						|
    const jquery_helper = make_jquery_helper();
 | 
						|
    const sidebar_helper = make_sidebar_helper();
 | 
						|
    const topic_list_helper = make_topic_list_helper();
 | 
						|
 | 
						|
    // This is what we are testing!
 | 
						|
    stream_list.update_streams_sidebar();
 | 
						|
 | 
						|
    jquery_helper.verify_actions();
 | 
						|
    sidebar_helper.verify_actions();
 | 
						|
    topic_list_helper.verify_actions();
 | 
						|
});
 |