mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 08:33:43 +00:00
Particularly when grouping in Sentry, pushing the ids and such into the additional data section helps group like errors together better.
907 lines
26 KiB
JavaScript
907 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
const {strict: assert} = require("assert");
|
|
|
|
const {mock_esm, mock_jquery, zrequire} = require("./lib/namespace");
|
|
const {run_test} = require("./lib/test");
|
|
const blueslip = require("./lib/zblueslip");
|
|
|
|
// 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.empty = () => {};
|
|
|
|
// 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.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);
|
|
};
|
|
};
|
|
|
|
$scroll_container.off = (ev) => {
|
|
assert.equal(ev, "scroll.list_widget_container");
|
|
$scroll_container.cleared = true;
|
|
};
|
|
|
|
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.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;
|
|
};
|
|
|
|
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);
|
|
};
|
|
};
|
|
|
|
return $element;
|
|
}
|
|
|
|
function div(item) {
|
|
return "<div>" + item + "</div>";
|
|
}
|
|
|
|
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: (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.scrollTop = 180;
|
|
$scroll_container.clientHeight = 100;
|
|
$scroll_container.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: (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.scrollTop = 180;
|
|
$scroll_container.clientHeight = 100;
|
|
$scroll_container.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();
|
|
|
|
const list = ["apple", "banana", "carrot", "dog", "egg", "fence", "grape"];
|
|
const opts = {
|
|
filter: {
|
|
$element: $search_input,
|
|
predicate: (item, value) => item.includes(value),
|
|
},
|
|
modifier: (item) => div(item),
|
|
$simplebar_container: $scroll_container,
|
|
};
|
|
|
|
const widget = ListWidget.create($container, list, opts);
|
|
|
|
let expected_html =
|
|
"<div>apple</div>" +
|
|
"<div>banana</div>" +
|
|
"<div>carrot</div>" +
|
|
"<div>dog</div>" +
|
|
"<div>egg</div>" +
|
|
"<div>fence</div>" +
|
|
"<div>grape</div>";
|
|
|
|
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.deepEqual(widget.get_current_list(), ["dog", "egg", "grape"]);
|
|
expected_html = "<div>dog</div><div>egg</div><div>grape</div>";
|
|
assert.deepEqual($container.$appended_data.html(), expected_html);
|
|
|
|
// 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 = "<div>greta</div><div>gary</div><div>giraffe</div>";
|
|
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: (item) => div(item),
|
|
$simplebar_container: $scroll_container,
|
|
callback_after_render() {
|
|
callback_called = true;
|
|
},
|
|
};
|
|
const widget = ListWidget.create($container, ["apple", "banana"], opts);
|
|
widget.render();
|
|
assert.deepEqual(callback_called, true);
|
|
|
|
const expected_html = "<div>apple</div><div>banana</div>";
|
|
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 data(sel) {
|
|
switch (sel) {
|
|
case "sort":
|
|
return opts.sort_type;
|
|
case "sort-prop":
|
|
return opts.prop_name;
|
|
/* istanbul ignore next */
|
|
default:
|
|
throw new Error("unknown selector: " + sel);
|
|
}
|
|
}
|
|
|
|
function lookup(sel, value) {
|
|
return (selector) => {
|
|
assert.equal(sel, selector);
|
|
return value;
|
|
};
|
|
}
|
|
|
|
const classList = new Set();
|
|
|
|
const $button = {
|
|
data,
|
|
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: (s) => "(" + s + ")",
|
|
$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: (item) => div(item.name) + div(item.salary),
|
|
filter: {
|
|
predicate: () => true,
|
|
},
|
|
$simplebar_container: $scroll_container,
|
|
};
|
|
|
|
function html_for(people) {
|
|
return people.map((item) => opts.modifier(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);
|
|
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;
|
|
}
|
|
|
|
ListWidget.create($container, list, {
|
|
name: "custom-sort-list",
|
|
modifier: (n) => "(" + n.x + ", " + n.y + ")",
|
|
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)");
|
|
|
|
const widget = ListWidget.get("custom-sort-list");
|
|
|
|
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() {},
|
|
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);
|
|
|
|
// 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);
|
|
});
|
|
|
|
run_test("errors", () => {
|
|
// We don't care about actual data for this test.
|
|
const list = ["stub"];
|
|
const $container = make_container();
|
|
const $scroll_container = make_scroll_container();
|
|
|
|
blueslip.expect("error", "Need opts to create widget.");
|
|
ListWidget.create($container, list);
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "$simplebar_container is missing.");
|
|
ListWidget.create($container, list, {
|
|
modifier: "hello world",
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "get_item should be a function");
|
|
ListWidget.create($container, list, {
|
|
get_item: "not a function",
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filter predicate is not a function.");
|
|
ListWidget.create($container, list, {
|
|
filter: {
|
|
predicate: "wrong type",
|
|
},
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filterer and predicate are mutually exclusive.");
|
|
ListWidget.create($container, list, {
|
|
filter: {
|
|
filterer: /* istanbul ignore next */ () => true,
|
|
predicate: /* istanbul ignore next */ () => true,
|
|
},
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filter filterer is not a function (or missing).");
|
|
ListWidget.create($container, list, {
|
|
filter: {},
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "List item is not a string");
|
|
ListWidget.create($container, list, {
|
|
modifier: () => 999,
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
});
|
|
|
|
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;
|
|
|
|
ListWidget.create($container, list, {
|
|
name: "replace-list",
|
|
modifier: (n) => "(" + n.toString() + ")",
|
|
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)");
|
|
|
|
const widget = ListWidget.get("replace-list");
|
|
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 = (query) => {
|
|
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 = `<tr data-item=${item}>updated: ${item}</tr>\n`;
|
|
const regex = new RegExp(`\\<tr data-item=${item}\\>.*?<\\/tr\\>`);
|
|
assert.ok(expected_queries.includes(query));
|
|
if (query.includes(`data-item='${INITIAL_RENDER_COUNT}'`)) {
|
|
return undefined; // This item is not rendered, so we find nothing
|
|
}
|
|
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(html) {
|
|
assert.equal(new_html, html);
|
|
called = true;
|
|
$container.$appended_data.replace(regex, new_html);
|
|
},
|
|
};
|
|
};
|
|
|
|
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: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
|
|
get_item,
|
|
html_selector: (item) => `tr[data-item='${item}']`,
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
const item = INITIAL_RENDER_COUNT - 1;
|
|
|
|
assert.ok($container.$appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
|
|
assert.ok($container.$appended_data.html().includes("<tr data-item=3>initial: 3</tr>"));
|
|
text = "updated";
|
|
called = false;
|
|
widget.render_item(INITIAL_RENDER_COUNT - 1);
|
|
assert.ok(called);
|
|
assert.ok($container.$appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
|
|
assert.ok(
|
|
$container.$appended_data.html().includes(`<tr data-item=${item}>updated: ${item}</tr>`),
|
|
);
|
|
|
|
// Item 80 should not be in the rendered list. (0 indexed)
|
|
assert.ok(
|
|
!$container.$appended_data
|
|
.html()
|
|
.includes(
|
|
`<tr data-item=${INITIAL_RENDER_COUNT}>initial: ${INITIAL_RENDER_COUNT}</tr>`,
|
|
),
|
|
);
|
|
called = false;
|
|
widget.render_item(INITIAL_RENDER_COUNT);
|
|
assert.ok(!called);
|
|
widget.render_item(INITIAL_RENDER_COUNT - 1);
|
|
assert.ok(called);
|
|
|
|
// Tests below this are for the corner cases, where we abort the rerender.
|
|
|
|
blueslip.expect("error", "html_selector should be a function.");
|
|
ListWidget.create($container, list, {
|
|
name: "replace-list",
|
|
modifier: /* istanbul ignore next */ (item) =>
|
|
`<tr data-item=${item.value}>${item.text}</tr>\n`,
|
|
get_item,
|
|
html_selector: "hello world",
|
|
$simplebar_container: $scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
let get_item_called;
|
|
const widget_2 = ListWidget.create($container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\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: (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 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: (item) => `<li data-value="${item.value}">${item.name}</li>\n`,
|
|
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: "<i>",
|
|
},
|
|
];
|
|
|
|
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);
|
|
});
|