"use strict";
const assert = require("node:assert/strict");
const {mock_esm, mock_jquery, zrequire} = require("./lib/namespace.cjs");
const {run_test, noop} = require("./lib/test.cjs");
const blueslip = require("./lib/zblueslip.cjs");
const $ = require("./lib/zjquery.cjs");
// We need these stubs to get by instanceof checks.
// The ListWidget library allows you to insert objects
// that are either jQuery, Element, or just raw HTML
// strings.  We initially test with raw strings.
const scroll_util = mock_esm("../src/scroll_util");
// We only need very simple jQuery wrappers for when the
// "real" code wraps html or sets up click handlers.
// We'll simulate most other objects ourselves.
mock_jquery((arg) => {
    if (arg.to_jquery) {
        return arg.to_jquery();
    }
    return {
        addClass() {
            return this;
        },
        replace(regex, string) {
            arg = arg.replace(regex, string);
        },
        html: () => arg,
    };
});
const ListWidget = zrequire("list_widget");
// We build objects here that simulate jQuery containers.
// The main thing to do at first is simulate that our
// scroll container is the nearest ancestor to our main
// container that has a max-height attribute, and then
// the scroll container will have a scroll event attached to
// it.  This is a good time to read set_up_event_handlers
// in the real code.
function make_container() {
    const $container = {};
    $container.attr = noop;
    $container.empty = noop;
    $container.data = noop;
    // Make our append function just set a field we can
    // check in our tests.
    $container.append = ($data) => {
        $container.$appended_data = $data;
    };
    return $container;
}
function make_scroll_container() {
    const $scroll_container = {};
    $scroll_container[0] = {};
    $scroll_container.cleared = false;
    // Capture the scroll callback so we can call it in
    // our tests.
    $scroll_container.on = (ev, f) => {
        assert.equal(ev, "scroll.list_widget_container");
        $scroll_container.call_scroll = () => {
            f.call($scroll_container[0]);
        };
    };
    $scroll_container.off = (ev) => {
        assert.equal(ev, "scroll.list_widget_container");
        $scroll_container.cleared = true;
    };
    $scroll_container.is = () => false;
    return $scroll_container;
}
function make_sort_container() {
    const $sort_container = {};
    $sort_container.cleared = false;
    $sort_container.on = (ev, sel, f) => {
        assert.equal(ev, "click.list_widget_sort");
        assert.equal(sel, "[data-sort]");
        $sort_container.f = f;
    };
    $sort_container.off = (ev) => {
        assert.equal(ev, "click.list_widget_sort");
        $sort_container.cleared = true;
    };
    return $sort_container;
}
function make_filter_element() {
    const $element = {};
    $element.cleared = false;
    $element.clear_button_elem_cleared = false;
    $element.on = (ev, f) => {
        assert.equal(ev, "input.list_widget_filter");
        $element.f = f;
    };
    $element.off = (ev) => {
        assert.equal(ev, "input.list_widget_filter");
        $element.cleared = true;
    };
    const clear_input_element = {};
    clear_input_element.on = (ev, f) => {
        assert.equal(ev, "click");
        clear_input_element.f = f;
    };
    clear_input_element.off = (ev) => {
        assert.equal(ev, "click");
        $element.clear_button_elem_cleared = true;
    };
    $element.siblings = (selector) => {
        assert.equal(selector, ".clear-filter");
        return clear_input_element;
    };
    return $element;
}
function make_search_input() {
    const $element = {};
    // Allow ourselves to be wrapped by $(...) and
    // return ourselves.
    /* istanbul ignore next */
    $element.to_jquery = () => $element;
    $element.on = (event_name, f) => {
        assert.equal(event_name, "input.list_widget_filter");
        $element.simulate_input_event = () => {
            const elem = {
                value: $element.val(),
            };
            f.call(elem);
        };
    };
    const clear_search_button_element = {};
    clear_search_button_element.on = (event, f) => {
        assert.equal(event, "click");
        $element.simulate_clear_search = () => {
            f.call();
        };
    };
    $element.siblings = (selector) => {
        assert.equal(selector, ".clear-filter");
        return clear_search_button_element;
    };
    return $element;
}
function div(item) {
    return "
" + item + "
";
}
run_test("scrolling", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const items = [];
    let get_scroll_element_called = false;
    scroll_util.get_scroll_element = ($element) => {
        get_scroll_element_called = true;
        return $element;
    };
    for (let i = 0; i < 200; i += 1) {
        items.push("item " + i);
    }
    const opts = {
        modifier_html: (item) => item,
        get_item: (item) => item,
        $simplebar_container: $scroll_container,
    };
    ListWidget.create($container, items, opts);
    assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
    assert.equal(get_scroll_element_called, true);
    // Set up our fake geometry so it forces a scroll action.
    $scroll_container[0].scrollTop = 180;
    $scroll_container[0].clientHeight = 100;
    $scroll_container[0].scrollHeight = 260;
    // Scrolling gets the next two elements from the list into
    // our widget.
    $scroll_container.call_scroll();
    assert.deepEqual($container.$appended_data.html(), items.slice(80, 100).join(""));
});
run_test("not_scrolling", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const items = [];
    let get_scroll_element_called = false;
    scroll_util.get_scroll_element = ($element) => {
        get_scroll_element_called = true;
        return $element;
    };
    let post_scroll__pre_render_callback_called = false;
    const post_scroll__pre_render_callback = () => {
        post_scroll__pre_render_callback_called = true;
    };
    let get_min_load_count_called = false;
    const get_min_load_count = (_offset, load_count) => {
        get_min_load_count_called = true;
        return load_count;
    };
    for (let i = 0; i < 200; i += 1) {
        items.push("item " + i);
    }
    const opts = {
        modifier_html: (item) => item,
        get_item: (item) => item,
        $simplebar_container: $scroll_container,
        is_scroll_position_for_render: () => false,
        post_scroll__pre_render_callback,
        get_min_load_count,
    };
    ListWidget.create($container, items, opts);
    assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
    assert.equal(get_scroll_element_called, true);
    // Set up our fake geometry.
    $scroll_container[0].scrollTop = 180;
    $scroll_container[0].clientHeight = 100;
    $scroll_container[0].scrollHeight = 260;
    // Since `should_render` is always false, no elements will be
    // added regardless of scrolling.
    $scroll_container.call_scroll();
    // $appended_data remains the same.
    assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
    assert.equal(post_scroll__pre_render_callback_called, true);
    assert.equal(get_min_load_count_called, true);
});
run_test("filtering", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $search_input = make_search_input();
    let last_filter_value = "";
    const list = ["apple", "banana", "carrot", "dog", "egg", "fence", "grape"];
    const opts = {
        filter: {
            $element: $search_input,
            predicate: (item, value) => item.includes(value),
        },
        modifier_html(item, filter_value) {
            last_filter_value = filter_value;
            return div(item);
        },
        get_item: (item) => item,
        $simplebar_container: $scroll_container,
    };
    const widget = ListWidget.create($container, list, opts);
    let expected_html =
        "apple
" +
        "banana
" +
        "carrot
" +
        "dog
" +
        "egg
" +
        "fence
" +
        "grape
";
    assert.deepEqual($container.$appended_data.html(), expected_html);
    // Filtering will pick out dog/egg/grape when we put "g"
    // into our search input.  (This uses the default filter, which
    // is a glorified indexOf call.)
    $search_input.val = () => "g";
    $search_input.simulate_input_event();
    assert.equal(last_filter_value, "g");
    assert.deepEqual(widget.get_current_list(), ["dog", "egg", "grape"]);
    expected_html = "dog
egg
grape
";
    assert.deepEqual($container.$appended_data.html(), expected_html);
    $search_input.simulate_clear_search();
    assert.deepEqual(widget.get_current_list(), list);
    // Reset the search input value to test that list is updated correctly.
    $search_input.val = () => "g";
    $search_input.simulate_input_event();
    // We can insert new data into the widget.
    const new_data = ["greta", "faye", "gary", "frank", "giraffe", "fox"];
    widget.replace_list_data(new_data);
    expected_html = "greta
gary
giraffe
";
    assert.deepEqual($container.$appended_data.html(), expected_html);
});
run_test("no filtering", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    let callback_called = false;
    // Opts does not require a filter key.
    const opts = {
        modifier_html: (item) => div(item),
        $simplebar_container: $scroll_container,
        callback_after_render() {
            callback_called = true;
        },
        get_item: (item) => item,
    };
    const widget = ListWidget.create($container, ["apple", "banana"], opts);
    widget.render();
    assert.deepEqual(callback_called, true);
    const expected_html = "apple
banana
";
    assert.deepEqual($container.$appended_data.html(), expected_html);
});
function sort_button(opts) {
    // The complications here are due to needing to find
    // the list via complicated HTML assumptions. Also, we
    // don't have any abstraction for the button and its
    // siblings other than direct jQuery actions.
    function attr(name) {
        switch (name) {
            case "data-sort":
                return opts.sort_type;
            case "data-sort-prop":
                return opts.prop_name;
            /* istanbul ignore next */
            default:
                throw new Error("unknown attribute: " + name);
        }
    }
    function lookup(sel, value) {
        return (selector) => {
            assert.equal(sel, selector);
            return value;
        };
    }
    const classList = new Set();
    const $button = {
        attr,
        closest: lookup(".progressive-table-wrapper", {
            data: lookup("list-widget", opts.list_name),
        }),
        addClass(cls) {
            classList.add(cls);
        },
        hasClass: (cls) => classList.has(cls),
        removeClass(cls) {
            classList.delete(cls);
        },
        siblings: lookup(".active", {
            removeClass(cls) {
                assert.equal(cls, "active");
                $button.siblings_deactivated = true;
            },
        }),
        siblings_deactivated: false,
        to_jquery: () => $button,
    };
    return $button;
}
run_test("wire up filter element", () => {
    const lst = ["alice", "JESSE", "moses", "scott", "Sean", "Xavier"];
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $filter_element = make_filter_element();
    const opts = {
        filter: {
            filterer: (list, value) => list.filter((item) => item.toLowerCase().includes(value)),
            $element: $filter_element,
        },
        modifier_html: (s) => "(" + s + ")",
        get_item: (item) => item,
        $simplebar_container: $scroll_container,
    };
    ListWidget.create($container, lst, opts);
    $filter_element.f.apply({value: "se"});
    assert.equal($container.$appended_data.html(), "(JESSE)(moses)(Sean)");
});
run_test("sorting", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $sort_container = make_sort_container();
    let cleared;
    $container.empty = () => {
        cleared = true;
    };
    const alice = {name: "alice", salary: 50};
    const bob = {name: "Bob", salary: 40};
    const cal = {name: "cal", salary: 30};
    const dave = {name: "dave", salary: 25};
    const ellen = {name: "ellen", salary: 95};
    const list = [bob, ellen, dave, alice, cal];
    const opts = {
        name: "sorting-list",
        $parent_container: $sort_container,
        modifier_html: (item) => div(item.name) + div(item.salary),
        get_item: (item) => item,
        filter: {
            predicate: () => true,
        },
        sort_fields: {
            ...ListWidget.generic_sort_functions("alphabetic", ["name"]),
            ...ListWidget.generic_sort_functions("numeric", ["salary"]),
        },
        $simplebar_container: $scroll_container,
    };
    function html_for(people) {
        return people.map((item) => opts.modifier_html(item)).join("");
    }
    ListWidget.create($container, list, opts);
    let button_opts;
    let $button;
    let expected_html;
    button_opts = {
        sort_type: "alphabetic",
        prop_name: "name",
        list_name: "my-list",
        active: false,
    };
    $button = sort_button(button_opts);
    $sort_container.f.apply($button);
    assert.ok(cleared);
    assert.ok($button.siblings_deactivated);
    expected_html = html_for([alice, bob, cal, dave, ellen]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    // Hit same button again to reverse the data.
    cleared = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    expected_html = html_for([ellen, dave, cal, bob, alice]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    assert.ok($button.hasClass("descend"));
    // And then hit a third time to go back to the forward sort.
    cleared = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    expected_html = html_for([alice, bob, cal, dave, ellen]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    assert.ok(!$button.hasClass("descend"));
    // Now try a numeric sort.
    button_opts = {
        sort_type: "numeric",
        prop_name: "salary",
        list_name: "my-list",
        active: false,
    };
    $button = sort_button(button_opts);
    cleared = false;
    $button.siblings_deactivated = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    assert.ok($button.siblings_deactivated);
    expected_html = html_for([dave, cal, bob, alice, ellen]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    // Hit same button again to reverse the numeric sort.
    cleared = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    expected_html = html_for([ellen, alice, bob, cal, dave]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
});
run_test("Apply consecutive sorts", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $sort_container = make_sort_container();
    let cleared;
    $container.empty = () => {
        cleared = true;
    };
    const alice = {name: "alice", salary: 50};
    const bob = {name: "bob", salary: 40};
    const cal = {name: "cal", salary: 30};
    const dave = {name: "dave", salary: 20};
    const ellen = {name: "ellen", salary: 95};
    const bob_2 = {name: "bob", salary: 20};
    const cal_2 = {name: "cal", salary: 60};
    const list = [alice, bob, cal, dave, ellen, bob_2, cal_2];
    const opts = {
        name: "sorting-list",
        $parent_container: $sort_container,
        modifier_html: (item) => div(item.name) + div(item.salary),
        get_item: (item) => item,
        filter: {
            predicate: () => true,
        },
        sort_fields: {
            ...ListWidget.generic_sort_functions("alphabetic", ["name"]),
            ...ListWidget.generic_sort_functions("numeric", ["salary"]),
        },
        $simplebar_container: $scroll_container,
    };
    function html_for(people) {
        return people.map((item) => opts.modifier_html(item)).join("");
    }
    ListWidget.create($container, list, opts);
    let button_opts;
    let $button;
    let expected_html;
    // Apply sorting by salary first, then by name
    button_opts = {
        sort_type: "numeric",
        prop_name: "salary",
        list_name: "my-list",
        active: false,
    };
    $button = sort_button(button_opts);
    $sort_container.f.apply($button);
    button_opts = {
        sort_type: "alphabetic",
        prop_name: "name",
        list_name: "my-list",
        active: false,
    };
    $button = sort_button(button_opts);
    $sort_container.f.apply($button);
    assert.ok(cleared);
    assert.ok($button.siblings_deactivated);
    expected_html = html_for([alice, bob_2, bob, cal, cal_2, dave, ellen]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    // Apply sorting by salary again, the previous sort by salary should be removed
    button_opts = {
        sort_type: "numeric",
        prop_name: "salary",
        list_name: "my-list",
        active: false,
    };
    $button = sort_button(button_opts);
    cleared = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    expected_html = html_for([bob_2, dave, cal, bob, alice, cal_2, ellen]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    assert.ok(!$button.hasClass("descend"));
    // Hit the salary field again to reverse the salary sorting
    cleared = false;
    $sort_container.f.apply($button);
    assert.ok(cleared);
    expected_html = html_for([ellen, cal_2, alice, bob, cal, bob_2, dave]);
    assert.deepEqual($container.$appended_data.html(), expected_html);
    assert.ok($button.hasClass("descend"));
});
run_test("custom sort", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const n42 = {x: 6, y: 7};
    const n43 = {x: 1, y: 43};
    const n44 = {x: 4, y: 11};
    const list = [n42, n43, n44];
    function sort_by_x(a, b) {
        return a.x - b.x;
    }
    function sort_by_product(a, b) {
        return a.x * a.y - b.x * b.y;
    }
    const widget = ListWidget.create($container, list, {
        name: "custom-sort-list",
        modifier_html: (n) => "(" + n.x + ", " + n.y + ")",
        get_item: (item) => item,
        sort_fields: {
            product: sort_by_product,
            x_value: sort_by_x,
        },
        init_sort: sort_by_product,
        $simplebar_container: $scroll_container,
    });
    assert.deepEqual($container.$appended_data.html(), "(6, 7)(1, 43)(4, 11)");
    widget.sort("x_value");
    assert.deepEqual($container.$appended_data.html(), "(1, 43)(4, 11)(6, 7)");
    // We can sort without registering the function, too.
    function sort_by_y(a, b) {
        return a.y - b.y;
    }
    widget.sort(sort_by_y);
    assert.deepEqual($container.$appended_data.html(), "(6, 7)(4, 11)(1, 43)");
});
run_test("clear_event_handlers", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $sort_container = make_sort_container();
    const $filter_element = make_filter_element();
    // We don't care about actual data for this test.
    const list = [];
    const opts = {
        name: "list-we-create-twice",
        $parent_container: $sort_container,
        modifier_html() {},
        get_item() {},
        filter: {
            $element: $filter_element,
            predicate: /* istanbul ignore next */ () => true,
        },
        $simplebar_container: $scroll_container,
    };
    // Create it the first time.
    ListWidget.create($container, list, opts);
    assert.equal($sort_container.cleared, false);
    assert.equal($scroll_container.cleared, false);
    assert.equal($filter_element.cleared, false);
    assert.equal($filter_element.clear_button_elem_cleared, false);
    // The second time we'll clear the old events.
    ListWidget.create($container, list, opts);
    assert.equal($sort_container.cleared, true);
    assert.equal($scroll_container.cleared, true);
    assert.equal($filter_element.cleared, true);
    assert.equal($filter_element.clear_button_elem_cleared, true);
});
run_test("sort helpers", () => {
    /*
        We mostly test our sorting helpers using the
        actual widget, but this test gets us a bit
        more line coverage.
    */
    const alice2 = {name: "alice", id: 2};
    const alice10 = {name: "alice", id: 10};
    const bob2 = {name: "bob", id: 2};
    const bob10 = {name: "bob", id: 10};
    const alpha_cmp = ListWidget.alphabetic_sort("name");
    const num_cmp = ListWidget.numeric_sort("id");
    assert.equal(alpha_cmp(alice2, alice10), 0);
    assert.equal(alpha_cmp(alice2, bob2), -1);
    assert.equal(alpha_cmp(bob2, alice10), 1);
    assert.equal(num_cmp(alice2, bob2), 0);
    assert.equal(num_cmp(alice2, bob10), -1);
    assert.equal(num_cmp(alice10, bob2), 1);
});
run_test("replace_list_data w/filter update", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const list = [1, 2, 3, 4];
    let num_updates = 0;
    const widget = ListWidget.create($container, list, {
        name: "replace-list",
        modifier_html: (n) => "(" + n.toString() + ")",
        get_item: (item) => item,
        filter: {
            predicate: (n) => n % 2 === 0,
            onupdate() {
                num_updates += 1;
            },
        },
        $simplebar_container: $scroll_container,
    });
    assert.equal(num_updates, 0);
    assert.deepEqual($container.$appended_data.html(), "(2)(4)");
    widget.replace_list_data([5, 6, 7, 8]);
    assert.equal(num_updates, 1);
    assert.deepEqual($container.$appended_data.html(), "(6)(8)");
});
run_test("opts.get_item", () => {
    const items = {};
    items[1] = "one";
    items[2] = "two";
    items[3] = "three";
    items[4] = "four";
    const list = [1, 2, 3, 4];
    const boring_opts = {
        get_item: (n) => items[n],
    };
    assert.deepEqual(ListWidget.get_filtered_items("whatever", list, boring_opts), [
        "one",
        "two",
        "three",
        "four",
    ]);
    const predicate = (item, value) => item.startsWith(value);
    const predicate_opts = {
        get_item: (n) => items[n],
        filter: {
            predicate,
        },
    };
    assert.deepEqual(ListWidget.get_filtered_items("t", list, predicate_opts), ["two", "three"]);
    const filterer_opts = {
        get_item: (n) => items[n],
        filter: {
            filterer: (items, value) => items.filter((item) => predicate(item, value)),
        },
    };
    assert.deepEqual(ListWidget.get_filtered_items("t", list, filterer_opts), ["two", "three"]);
});
run_test("render item", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const INITIAL_RENDER_COUNT = 80; // Keep this in sync with the actual code.
    let called = false;
    $scroll_container.find = (element) => {
        const query = element.selector;
        const expected_queries = [
            `tr[data-item='${INITIAL_RENDER_COUNT}']`,
            `tr[data-item='${INITIAL_RENDER_COUNT - 1}']`,
        ];
        const item = INITIAL_RENDER_COUNT - 1;
        const new_html = `updated: ${item}
\n`;
        const regex = new RegExp(`\\.*?<\\/tr\\>`);
        assert.ok(expected_queries.includes(query));
        if (query.includes(`data-item='${INITIAL_RENDER_COUNT}'`)) {
            // This item is not rendered, so we find nothing so return an empty stub.
            return {
                length: 0,
            };
        }
        return {
            // Return a JQuery stub for the original HTML.
            // We want this to be called when we replace
            // the existing HTML with newly rendered HTML.
            replaceWith($element) {
                assert.equal(new_html, $element.html());
                called = true;
                $container.$appended_data.replace(regex, new_html);
            },
            length: 1,
        };
    };
    const list = [...Array.from({length: 100}).keys()];
    let text = "initial";
    const get_item = (item) => ({text: `${text}: ${item}`, value: item});
    const widget = ListWidget.create($container, list, {
        name: "replace-list",
        modifier_html: (item) => `
${item.text}
\n`,
        get_item,
        html_selector: (item) => $(`tr[data-item='${item.value}']`),
        $simplebar_container: $scroll_container,
    });
    const item = INITIAL_RENDER_COUNT - 1;
    assert.ok($container.$appended_data.html().includes("initial: 2
"));
    assert.ok($container.$appended_data.html().includes("initial: 3
"));
    text = "updated";
    called = false;
    widget.render_item(get_item(INITIAL_RENDER_COUNT - 1));
    assert.ok(called);
    assert.ok($container.$appended_data.html().includes("initial: 2
"));
    assert.ok(
        $container.$appended_data.html().includes(`updated: ${item}
`),
    );
    // Item 80 should not be in the rendered list. (0 indexed)
    assert.ok(
        !$container.$appended_data
            .html()
            .includes(
                `initial: ${INITIAL_RENDER_COUNT}
`,
            ),
    );
    called = false;
    widget.render_item(get_item(INITIAL_RENDER_COUNT));
    assert.ok(!called);
    widget.render_item(get_item(INITIAL_RENDER_COUNT - 1));
    assert.ok(called);
    // Tests below this are for the corner cases, where we abort the rerender.
    let get_item_called;
    const widget_2 = ListWidget.create($container, list, {
        name: "replace-list",
        modifier_html: (item) => `${item.text}
\n`,
        get_item(item) {
            get_item_called = true;
            return item;
        },
        $simplebar_container: $scroll_container,
    });
    get_item_called = false;
    widget_2.render_item(item);
    // Test that we didn't try to render the item.
    assert.ok(!get_item_called);
    let rendering_item = false;
    const widget_3 = ListWidget.create($container, list, {
        name: "replace-list",
        modifier_html: (item) => (rendering_item ? undefined : `${item}\n`),
        get_item,
        html_selector: (item) => $(`tr[data-item='${item}']`),
        $simplebar_container: $scroll_container,
    });
    // Once we have initially rendered the widget, change the
    // behavior of the modifier_html function.
    rendering_item = true;
    blueslip.expect("error", "List item is not a string");
    widget_3.render_item(item);
    blueslip.reset();
});
run_test("Multiselect dropdown retain_selected_items", () => {
    const $container = make_container();
    const $scroll_container = make_scroll_container();
    const $filter_element = make_filter_element();
    let data_rendered = [];
    const list = ["one", "two", "three", "four"].map((x) => ({name: x, value: x}));
    const data = ["one"]; // Data initially selected.
    $container.find = (elem) => DropdownItem(elem);
    // We essentially create fake jQuery functions
    // whose return value are stored in objects so that
    // they can be later asserted with expected values.
    function DropdownItem(element) {
        const temp = {};
        function length() {
            if (element) {
                return true;
            }
            /* istanbul ignore next */
            return false;
        }
        function find(tag) {
            return ListItem(tag, temp);
        }
        function addClass(cls) {
            temp.appended_class = cls;
        }
        temp.element = element;
        return {
            length: length(),
            find,
            addClass,
        };
    }
    function ListItem(element, temp) {
        function expectOne() {
            data_rendered.push(temp);
            return ListItem(element, temp);
        }
        function prepend($data) {
            temp.prepended_data = $data.html();
        }
        return {
            expectOne,
            prepend,
        };
    }
    const widget = ListWidget.create($container, list, {
        name: "replace-list",
        modifier_html: (item) => `${item.name}\n`,
        get_item: (item) => item,
        multiselect: {
            selected_items: data,
        },
        filter: {
            $element: $filter_element,
            predicate: () => true,
        },
        $simplebar_container: $scroll_container,
    });
    const expected_value = [
        {
            element: 'li[data-value="one"]',
            appended_class: "checked",
            prepended_data: "",
        },
    ];
    assert.deepEqual(expected_value, data_rendered);
    // Reset the variable and re execute the `widget.render` method.
    data_rendered = [];
    // Making sure!
    assert.deepEqual(data_rendered, []);
    widget.hard_redraw();
    // Expect the `data_rendered` array to be same again.
    assert.deepEqual(expected_value, data_rendered);
});