2018-03-29 15:05:37 +02:00
|
|
|
set_global('i18n', global.stub_i18n);
|
|
|
|
|
2018-04-04 16:36:49 +02:00
|
|
|
zrequire('keydown_util');
|
2018-03-29 15:05:37 +02:00
|
|
|
zrequire('components');
|
|
|
|
|
2018-04-04 16:36:49 +02:00
|
|
|
var noop = function () {};
|
|
|
|
|
2018-12-18 19:34:45 +01:00
|
|
|
var LEFT_KEY = { which: 37, preventDefault: noop, stopPropagation: noop };
|
|
|
|
var RIGHT_KEY = { which: 39, preventDefault: noop, stopPropagation: noop };
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2018-05-15 12:40:07 +02:00
|
|
|
run_test('basics', () => {
|
2018-03-29 15:05:37 +02:00
|
|
|
var keydown_f;
|
|
|
|
var click_f;
|
|
|
|
var tabs = [];
|
|
|
|
var focused_tab;
|
|
|
|
var callback_args;
|
|
|
|
|
|
|
|
function make_tab(i) {
|
|
|
|
var self = {};
|
|
|
|
|
|
|
|
assert.equal(tabs.length, i);
|
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
self.class = [];
|
|
|
|
|
|
|
|
self.addClass = function (c) {
|
|
|
|
self.class += ' ' + c;
|
|
|
|
var tokens = self.class.trim().split(/ +/);
|
|
|
|
self.class = _.uniq(tokens).join(' ');
|
|
|
|
};
|
|
|
|
|
|
|
|
self.removeClass = function (c) {
|
|
|
|
var tokens = self.class.trim().split(/ +/);
|
|
|
|
self.class = _.without(tokens, c).join(' ');
|
|
|
|
};
|
|
|
|
|
2019-05-08 09:26:27 +02:00
|
|
|
self.hasClass = function (c) {
|
|
|
|
var tokens = self.class.trim().split(/ +/);
|
|
|
|
return tokens.includes(c);
|
|
|
|
};
|
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
self.data = function (name) {
|
|
|
|
assert.equal(name, 'tab-id');
|
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.focus = function () {
|
|
|
|
focused_tab = i;
|
|
|
|
};
|
|
|
|
|
|
|
|
tabs.push(self);
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
var ind_tab = (function () {
|
|
|
|
var self = {};
|
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
|
|
|
|
self.click = function (f) {
|
|
|
|
click_f = f;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.keydown = function (f) {
|
|
|
|
keydown_f = f;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.removeClass = function (c) {
|
|
|
|
_.each(tabs, function (tab) {
|
|
|
|
tab.removeClass(c);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-03-29 20:48:49 +02:00
|
|
|
self.eq = function (idx) {
|
|
|
|
return tabs[idx];
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}());
|
|
|
|
|
|
|
|
var switcher = (function () {
|
|
|
|
var self = {};
|
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
|
|
|
|
self.children = [];
|
|
|
|
|
|
|
|
self.append = function (child) {
|
|
|
|
self.children.push(child);
|
|
|
|
};
|
|
|
|
|
|
|
|
self.find = function (sel) {
|
|
|
|
switch (sel) {
|
|
|
|
case ".ind-tab":
|
|
|
|
return ind_tab;
|
|
|
|
default:
|
|
|
|
throw Error('unknown selector: ' + sel);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}());
|
|
|
|
|
|
|
|
set_global('$', function (sel) {
|
|
|
|
if (sel.stub) {
|
|
|
|
// The component often redundantly re-wraps objects.
|
|
|
|
return sel;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (sel) {
|
|
|
|
case "<div class='tab-switcher'></div>":
|
|
|
|
return switcher;
|
|
|
|
case "<div class='ind-tab' data-tab-key='keyboard-shortcuts' data-tab-id='0' tabindex='0'>translated: Keyboard shortcuts</div>":
|
|
|
|
return make_tab(0);
|
2018-08-29 19:15:25 +02:00
|
|
|
case "<div class='ind-tab' data-tab-key='message-formatting' data-tab-id='1' tabindex='0'>translated: Message formatting</div>":
|
2018-03-29 15:05:37 +02:00
|
|
|
return make_tab(1);
|
|
|
|
case "<div class='ind-tab' data-tab-key='search-operators' data-tab-id='2' tabindex='0'>translated: Search operators</div>":
|
|
|
|
return make_tab(2);
|
|
|
|
default:
|
|
|
|
throw Error('unknown selector: ' + sel);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-04-04 21:06:58 +02:00
|
|
|
var callback_value;
|
|
|
|
|
|
|
|
var widget;
|
|
|
|
widget = components.toggle({
|
2018-03-29 15:05:37 +02:00
|
|
|
selected: 0,
|
|
|
|
values: [
|
|
|
|
{ label: i18n.t("Keyboard shortcuts"), key: "keyboard-shortcuts" },
|
2018-08-29 19:15:25 +02:00
|
|
|
{ label: i18n.t("Message formatting"), key: "message-formatting" },
|
2018-03-29 15:05:37 +02:00
|
|
|
{ label: i18n.t("Search operators"), key: "search-operators" },
|
|
|
|
],
|
|
|
|
callback: function (name, key) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
assert.equal(focused_tab, 0);
|
|
|
|
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');
|
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
2018-08-29 19:15:25 +02:00
|
|
|
widget.goto('message-formatting');
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(focused_tab, 1);
|
|
|
|
assert.equal(tabs[0].class, 'first');
|
|
|
|
assert.equal(tabs[1].class, 'middle selected');
|
|
|
|
assert.equal(tabs[2].class, 'last');
|
2018-08-29 19:15:25 +02:00
|
|
|
assert.deepEqual(callback_args, ['translated: Message formatting', 'message-formatting']);
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(widget.value(), 'translated: Message formatting');
|
|
|
|
|
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;
|
2018-08-29 19:15:25 +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);
|
|
|
|
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);
|
|
|
|
assert.equal(widget.value(), 'translated: Search operators');
|
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
|
|
|
assert.equal(widget.value(), 'translated: Message formatting');
|
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
|
|
|
assert.equal(widget.value(), 'translated: Keyboard shortcuts');
|
|
|
|
|
|
|
|
// try to crash the key handler
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
|
|
|
assert.equal(widget.value(), 'translated: Keyboard shortcuts');
|
2018-03-29 20:48:49 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
click_f.call(tabs[1]);
|
|
|
|
assert.equal(widget.value(), 'translated: Message formatting');
|
2019-05-08 09:26:27 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
widget.disable_tab("search-operators");
|
|
|
|
assert.equal(tabs[2].hasClass('disabled'), true);
|
|
|
|
assert.equal(tabs[2].class, "last disabled");
|
|
|
|
|
|
|
|
widget.goto('keyboard-shortcuts');
|
|
|
|
assert.equal(focused_tab, 0);
|
|
|
|
widget.goto("search-operators");
|
|
|
|
assert.equal(focused_tab, 0);
|
2018-05-15 12:40:07 +02:00
|
|
|
});
|