2020-08-01 03:43:15 +02:00
|
|
|
"use strict";
|
|
|
|
|
2020-11-30 23:46:45 +01:00
|
|
|
const {strict: assert} = require("assert");
|
|
|
|
|
2020-07-25 02:02:35 +02:00
|
|
|
const _ = require("lodash");
|
|
|
|
|
2021-03-11 05:43:45 +01:00
|
|
|
const {mock_cjs, zrequire} = require("../zjsunit/namespace");
|
2020-12-01 00:39:47 +01:00
|
|
|
const {run_test} = require("../zjsunit/test");
|
2020-12-01 00:02:16 +01:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
let keydown_f;
|
|
|
|
let click_f;
|
|
|
|
const tabs = [];
|
|
|
|
let focused_tab;
|
|
|
|
|
|
|
|
function make_tab(i) {
|
|
|
|
const self = {};
|
|
|
|
|
|
|
|
assert.equal(tabs.length, i);
|
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
self.class = [];
|
|
|
|
|
|
|
|
self.addClass = (c) => {
|
|
|
|
self.class += " " + c;
|
|
|
|
const tokens = self.class.trim().split(/ +/);
|
|
|
|
self.class = _.uniq(tokens).join(" ");
|
|
|
|
};
|
|
|
|
|
|
|
|
self.removeClass = (c) => {
|
|
|
|
const tokens = self.class.trim().split(/ +/);
|
|
|
|
self.class = _.without(tokens, c).join(" ");
|
|
|
|
};
|
|
|
|
|
|
|
|
self.hasClass = (c) => {
|
|
|
|
const tokens = self.class.trim().split(/ +/);
|
|
|
|
return tokens.includes(c);
|
|
|
|
};
|
|
|
|
|
|
|
|
self.data = (name) => {
|
|
|
|
assert.equal(name, "tab-id");
|
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.text = (text) => {
|
|
|
|
assert.equal(
|
|
|
|
text,
|
|
|
|
[
|
|
|
|
"translated: Keyboard shortcuts",
|
|
|
|
"translated: Message formatting",
|
|
|
|
"translated: Search operators",
|
|
|
|
][i],
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
self.trigger = (type) => {
|
|
|
|
if (type === "focus") {
|
|
|
|
focused_tab = i;
|
|
|
|
}
|
|
|
|
};
|
2019-05-08 09:26:27 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
tabs.push(self);
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
return self;
|
|
|
|
}
|
2021-02-06 03:36:45 +01:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
const ind_tab = (function () {
|
|
|
|
const self = {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.stub = true;
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.on = (name, f) => {
|
|
|
|
if (name === "click") {
|
|
|
|
click_f = f;
|
|
|
|
} else if (name === "keydown") {
|
|
|
|
keydown_f = f;
|
|
|
|
}
|
|
|
|
};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.removeClass = (c) => {
|
|
|
|
for (const tab of tabs) {
|
|
|
|
tab.removeClass(c);
|
|
|
|
}
|
|
|
|
};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.eq = (idx) => tabs[idx];
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
return self;
|
|
|
|
})();
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
const switcher = (function () {
|
|
|
|
const self = {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.stub = true;
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.children = [];
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.classList = new Set();
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.append = (child) => {
|
|
|
|
self.children.push(child);
|
|
|
|
};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.addClass = (c) => {
|
|
|
|
self.classList.add(c);
|
|
|
|
self.addedClass = c;
|
|
|
|
};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
self.find = (sel) => {
|
|
|
|
switch (sel) {
|
|
|
|
case ".ind-tab":
|
|
|
|
return ind_tab;
|
|
|
|
default:
|
|
|
|
throw new Error("unknown selector: " + sel);
|
|
|
|
}
|
|
|
|
};
|
2020-04-20 22:41:03 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
return self;
|
|
|
|
})();
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:45 +01:00
|
|
|
mock_cjs("jquery", (sel, attributes) => {
|
2021-03-11 05:43:42 +01:00
|
|
|
if (sel.stub) {
|
|
|
|
// The component often redundantly re-wraps objects.
|
|
|
|
return sel;
|
|
|
|
}
|
2020-04-20 22:41:03 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
switch (sel) {
|
|
|
|
case "<div class='tab-switcher'></div>":
|
|
|
|
return switcher;
|
|
|
|
case "<div class='tab-switcher stream_sorter_toggle'></div>":
|
|
|
|
return switcher;
|
|
|
|
case "<div>": {
|
|
|
|
const tab_id = attributes["data-tab-id"];
|
|
|
|
assert.deepEqual(
|
|
|
|
attributes,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
class: "ind-tab",
|
|
|
|
"data-tab-key": "keyboard-shortcuts",
|
|
|
|
"data-tab-id": 0,
|
|
|
|
tabindex: 0,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
class: "ind-tab",
|
|
|
|
"data-tab-key": "message-formatting",
|
|
|
|
"data-tab-id": 1,
|
|
|
|
tabindex: 0,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
class: "ind-tab",
|
|
|
|
"data-tab-key": "search-operators",
|
|
|
|
"data-tab-id": 2,
|
|
|
|
tabindex: 0,
|
|
|
|
},
|
|
|
|
][tab_id],
|
|
|
|
);
|
|
|
|
return make_tab(tab_id);
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
throw new Error("unknown selector: " + sel);
|
|
|
|
}
|
|
|
|
});
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
const components = zrequire("components");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
const noop = () => {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
const LEFT_KEY = {which: 37, preventDefault: noop, stopPropagation: noop};
|
|
|
|
const RIGHT_KEY = {which: 39, preventDefault: noop, stopPropagation: noop};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2021-03-11 05:43:42 +01:00
|
|
|
run_test("basics", () => {
|
|
|
|
let callback_args;
|
2019-11-02 00:06:25 +01:00
|
|
|
let callback_value;
|
2018-04-04 21:06:58 +02:00
|
|
|
|
2019-10-26 01:49:36 +02:00
|
|
|
let widget = null;
|
2018-04-04 21:06:58 +02:00
|
|
|
widget = components.toggle({
|
2018-03-29 15:05:37 +02:00
|
|
|
selected: 0,
|
|
|
|
values: [
|
2020-07-16 22:40:18 +02:00
|
|
|
{label: i18n.t("Keyboard shortcuts"), key: "keyboard-shortcuts"},
|
|
|
|
{label: i18n.t("Message formatting"), key: "message-formatting"},
|
|
|
|
{label: i18n.t("Search operators"), key: "search-operators"},
|
2018-03-29 15:05:37 +02:00
|
|
|
],
|
2020-04-20 22:41:03 +02:00
|
|
|
html_class: "stream_sorter_toggle",
|
2020-07-20 22:18:43 +02:00
|
|
|
callback(name, key) {
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(callback_args, undefined);
|
|
|
|
callback_args = [name, key];
|
2018-04-04 21:06:58 +02:00
|
|
|
|
|
|
|
// The subs code tries to get a widget value in the middle of a
|
|
|
|
// callback, which can lead to obscure bugs.
|
|
|
|
if (widget) {
|
|
|
|
callback_value = widget.value();
|
|
|
|
}
|
2018-03-29 15:05:37 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
assert.equal(widget.get(), switcher);
|
|
|
|
|
|
|
|
assert.deepEqual(switcher.children, tabs);
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(switcher.addedClass, "stream_sorter_toggle");
|
2020-04-20 22:41:03 +02:00
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(focused_tab, 0);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first selected");
|
|
|
|
assert.equal(tabs[1].class, "middle");
|
|
|
|
assert.equal(tabs[2].class, "last");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Keyboard shortcuts", "keyboard-shortcuts"]);
|
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("message-formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(focused_tab, 1);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first");
|
|
|
|
assert.equal(tabs[1].class, "middle selected");
|
|
|
|
assert.equal(tabs[2].class, "last");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Message formatting", "message-formatting"]);
|
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
toggler: Always call back to callback function.
In our toggler component (the thing that handles tabs in things
like our markdown/search help, settings/org, etc.), we have
a callback mechanism when you switch to the tab. We were
being tricky and only calling it when the tab changed.
It turns out it's better to just always call the callback,
since these things are often in modals that open and close,
and if you open a modal for the second time, you want to do
the callback task for whichever setting you're going to.
There was actually kind of a nasty bug with this, where the
keyboard handling in the keyboard-help modal worked fine the
first time you opened it, but then it didn't work the second
time (if you focused some other element in the interim), and
it was due to not re-setting the focus to the inner modal
because we weren't calling the callback.
Of course, there are pitfalls in calling the same callbacks
twice, but our callbacks should generally be idempotent
for other reasons.
2018-06-03 22:12:10 +02:00
|
|
|
// Go to same tab twice and make sure we get callback.
|
2018-03-29 15:05:37 +02:00
|
|
|
callback_args = undefined;
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("message-formatting");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Message formatting", "message-formatting"]);
|
2018-03-29 15:05:37 +02:00
|
|
|
|
toggler: Always call back to callback function.
In our toggler component (the thing that handles tabs in things
like our markdown/search help, settings/org, etc.), we have
a callback mechanism when you switch to the tab. We were
being tricky and only calling it when the tab changed.
It turns out it's better to just always call the callback,
since these things are often in modals that open and close,
and if you open a modal for the second time, you want to do
the callback task for whichever setting you're going to.
There was actually kind of a nasty bug with this, where the
keyboard handling in the keyboard-help modal worked fine the
first time you opened it, but then it didn't work the second
time (if you focused some other element in the interim), and
it was due to not re-setting the focus to the inner modal
because we weren't calling the callback.
Of course, there are pitfalls in calling the same callbacks
twice, but our callbacks should generally be idempotent
for other reasons.
2018-06-03 22:12:10 +02:00
|
|
|
callback_args = undefined;
|
2018-03-29 15:05:37 +02:00
|
|
|
keydown_f.call(tabs[focused_tab], RIGHT_KEY);
|
|
|
|
assert.equal(focused_tab, 2);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first");
|
|
|
|
assert.equal(tabs[1].class, "middle");
|
|
|
|
assert.equal(tabs[2].class, "last selected");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Search operators", "search-operators"]);
|
|
|
|
assert.equal(widget.value(), "translated: Search operators");
|
2018-04-04 21:06:58 +02:00
|
|
|
assert.equal(widget.value(), callback_value);
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
// try to crash the key handler
|
|
|
|
keydown_f.call(tabs[focused_tab], RIGHT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Search operators");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
// try to crash the key handler
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 20:48:49 +02:00
|
|
|
|
2020-07-07 19:48:59 +02:00
|
|
|
callback_args = undefined;
|
|
|
|
widget.disable_tab("message-formatting");
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], RIGHT_KEY);
|
|
|
|
assert.equal(widget.value(), "translated: Search operators");
|
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
|
|
|
|
|
|
|
widget.enable_tab("message-formatting");
|
|
|
|
|
2018-03-29 20:48:49 +02:00
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
click_f.call(tabs[1]);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2019-05-08 09:26:27 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
widget.disable_tab("search-operators");
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[2].hasClass("disabled"), true);
|
2019-05-08 09:26:27 +02:00
|
|
|
assert.equal(tabs[2].class, "last disabled");
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("keyboard-shortcuts");
|
2019-05-08 09:26:27 +02:00
|
|
|
assert.equal(focused_tab, 0);
|
|
|
|
widget.goto("search-operators");
|
|
|
|
assert.equal(focused_tab, 0);
|
2018-05-15 12:40:07 +02:00
|
|
|
});
|