zulip/frontend_tests/node_tests/hotkey.js

436 lines
15 KiB
JavaScript
Raw Normal View History

// Important note on these tests:
//
// The way the Zulip hotkey tests work is as follows. First, we set
// up various contexts by monkey-patching the various hotkeys exports
// functions (like overlays.settings_open). Within that context, to
// test whether a given key (e.g. `x`) results in a specific function
// (e.g. `ui.foo()`), we fail to import any modules other than
// hotkey.js so that accessing them will result in a ReferenceError.
//
// Then we create a stub `ui.foo`, and call the hotkey function. If
// it calls any external module other than `ui.foo`, it'll crash.
// Future work includes making sure it actually does call `ui.foo()`.
set_global('activity', {
});
set_global('navigator', {
platform: '',
});
set_global('page_params', {
});
set_global('overlays', {
});
const noop = () => {};
2017-03-14 18:29:38 +01:00
2018-04-12 22:38:31 +02:00
// jQuery stuff should go away if we make an initialize() method.
set_global('document', 'document-stub');
set_global('$', global.make_zjquery());
$.fn.keydown = noop;
$.fn.keypress = noop;
2017-03-14 18:29:38 +01:00
const hotkey = zrequire('hotkey');
zrequire('common');
set_global('list_util', {
});
set_global('current_msg_list', {
selected_id: function () {
return 42;
},
selected_message: function () {
return {
sent_by_me: true,
flags: ["read", "starred"],
};
},
selected_row: function () {},
get_row: function () {
return 101;
},
});
function return_true() { return true; }
function return_false() { return false; }
function stubbing(func_name_to_stub, test_function) {
global.with_overrides(function (override) {
global.with_stub(function (stub) {
override(func_name_to_stub, stub.f);
test_function(stub);
});
});
}
run_test('mappings', () => {
function map_press(which, shiftKey) {
return hotkey.get_keypress_hotkey({
which: which,
shiftKey: shiftKey,
});
}
function map_down(which, shiftKey, ctrlKey, metaKey) {
return hotkey.get_keydown_hotkey({
which: which,
shiftKey: shiftKey,
2017-03-19 19:26:13 +01:00
ctrlKey: ctrlKey,
metaKey: metaKey,
});
}
// The next assertion protects against an iOS bug where we
// treat "!" as a hotkey, because iOS sends the wrong code.
assert.equal(map_press(33), undefined);
// Test page-up does work.
assert.equal(map_down(33).name, 'page_up');
// Test other mappings.
assert.equal(map_down(9).name, 'tab');
assert.equal(map_down(9, true).name, 'shift_tab');
assert.equal(map_down(27).name, 'escape');
assert.equal(map_down(37).name, 'left_arrow');
assert.equal(map_down(13).name, 'enter');
assert.equal(map_down(46).name, 'delete');
assert.equal(map_down(13, true).name, 'enter');
assert.equal(map_press(47).name, 'search'); // slash
assert.equal(map_press(106).name, 'vim_down'); // j
assert.equal(map_down(219, false, true).name, 'escape'); // ctrl + [
assert.equal(map_down(67, false, true).name, 'copy_with_c'); // ctrl + c
assert.equal(map_down(75, false, true).name, 'search_with_k'); // ctrl + k
assert.equal(map_down(83, false, true).name, 'star_message'); // ctrl + s
assert.equal(map_down(190, false, true).name, 'narrow_to_compose_target'); // ctrl + .
2017-03-19 19:26:13 +01:00
// More negative tests.
assert.equal(map_down(47), undefined);
assert.equal(map_press(27), undefined);
assert.equal(map_down(27, true), undefined);
2017-03-19 19:26:13 +01:00
assert.equal(map_down(86, false, true), undefined); // ctrl + v
assert.equal(map_down(90, false, true), undefined); // ctrl + z
assert.equal(map_down(84, false, true), undefined); // ctrl + t
assert.equal(map_down(82, false, true), undefined); // ctrl + r
assert.equal(map_down(79, false, true), undefined); // ctrl + o
assert.equal(map_down(80, false, true), undefined); // ctrl + p
assert.equal(map_down(65, false, true), undefined); // ctrl + a
assert.equal(map_down(70, false, true), undefined); // ctrl + f
assert.equal(map_down(72, false, true), undefined); // ctrl + h
assert.equal(map_down(88, false, true), undefined); // ctrl + x
assert.equal(map_down(78, false, true), undefined); // ctrl + n
assert.equal(map_down(77, false, true), undefined); // ctrl + m
assert.equal(map_down(67, false, false, true), undefined); // cmd + c
assert.equal(map_down(75, false, false, true), undefined); // cmd + k
assert.equal(map_down(83, false, false, true), undefined); // cmd + s
assert.equal(map_down(75, true, true), undefined); // shift + ctrl + k
assert.equal(map_down(83, true, true), undefined); // shift + ctrl + s
assert.equal(map_down(219, true, true, false), undefined); // shift + ctrl + [
// CMD tests for MacOS
global.navigator.platform = "MacIntel";
assert.equal(map_down(219, false, true, false).name, 'escape'); // ctrl + [
assert.equal(map_down(219, false, false, true), undefined); // cmd + [
assert.equal(map_down(67, false, true, true).name, 'copy_with_c'); // ctrl + c
assert.equal(map_down(67, false, true, false), undefined); // cmd + c
assert.equal(map_down(75, false, false, true).name, 'search_with_k'); // cmd + k
assert.equal(map_down(75, false, true, false), undefined); // ctrl + k
assert.equal(map_down(83, false, false, true).name, 'star_message'); // cmd + s
assert.equal(map_down(83, false, true, false), undefined); // ctrl + s
assert.equal(map_down(190, false, false, true).name, 'narrow_to_compose_target'); // cmd + .
assert.equal(map_down(190, false, true, false), undefined); // ctrl + .
// Reset platform
global.navigator.platform = '';
});
run_test('basic_chars', () => {
function process(s) {
const e = {
which: s.charCodeAt(0),
};
try {
return hotkey.process_keypress(e);
} catch (err) {
// An exception will be thrown here if a different
// function is called than the one declared. Try to
// provide a useful error message.
// add a newline to separate from other console output.
console.log('\nERROR: Mapping for character "' + e.which + '" does not match tests.');
}
}
function assert_mapping(c, func_name, shiftKey) {
stubbing(func_name, function () {
assert(process(c, shiftKey));
});
}
function assert_unmapped(s) {
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const c of s) {
assert.equal(process(c), false);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
}
// Unmapped keys should immediately return false, without
// calling any functions outside of hotkey.js.
assert_unmapped('abfhlmotyz');
assert_unmapped('BEFHILNOQTUWXYZ');
// We have to skip some checks due to the way the code is
// currently organized for mapped keys.
hotkey.in_content_editable_widget = return_false;
overlays.settings_open = return_false;
set_global('popovers', {
actions_popped: return_false,
message_info_popped: return_false,
});
set_global('emoji_picker', {
reactions_popped: return_false,
});
set_global('emoji_codes', {
codepoint_to_name: {
'1f44d': 'thumbs_up',
},
});
set_global('hotspots', {
is_open: return_false,
});
set_global('gear_menu', {
is_open: return_false,
});
// All letters should return false if we are composing text.
hotkey.processing_text = return_true;
function test_normal_typing() {
assert_unmapped('abcdefghijklmnopqrstuvwxyz');
assert_unmapped(' ');
assert_unmapped('[]\\.,;');
assert_unmapped('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
2017-09-07 03:01:17 +02:00
assert_unmapped('~!@#$%^*()_+{}:"<>');
}
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const settings_open of [return_true, return_false]) {
for (const is_active of [return_true, return_false]) {
for (const info_overlay_open of [return_true, return_false]) {
set_global('overlays', {
is_active: is_active,
settings_open: settings_open,
info_overlay_open: info_overlay_open,
});
test_normal_typing();
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
}
}
// Ok, now test keys that work when we're viewing messages.
hotkey.processing_text = return_false;
overlays.settings_open = return_false;
overlays.streams_open = return_false;
overlays.lightbox_open = return_false;
overlays.drafts_open = return_false;
page_params.can_create_streams = true;
overlays.streams_open = return_true;
overlays.is_active = return_true;
assert_mapping('S', 'subs.keyboard_sub');
assert_mapping('V', 'subs.view_stream');
assert_mapping('n', 'subs.open_create_stream');
page_params.can_create_streams = false;
assert_unmapped('n');
overlays.streams_open = return_false;
test_normal_typing();
overlays.is_active = return_false;
assert_mapping('?', 'info_overlay.maybe_show_keyboard_shortcuts');
assert_mapping('/', 'search.initiate_search');
assert_mapping('w', 'activity.initiate_search');
assert_mapping('q', 'stream_list.initiate_search');
assert_mapping('A', 'narrow.stream_cycle_backward');
assert_mapping('D', 'narrow.stream_cycle_forward');
2017-03-18 17:41:47 +01:00
assert_mapping('c', 'compose_actions.start');
assert_mapping('x', 'compose_actions.start');
2017-03-19 19:41:02 +01:00
assert_mapping('P', 'narrow.by');
assert_mapping('g', 'gear_menu.open');
overlays.is_active = return_true;
overlays.drafts_open = return_true;
assert_mapping('d', 'overlays.close_overlay');
overlays.drafts_open = return_false;
test_normal_typing();
overlays.is_active = return_false;
assert_mapping('d', 'drafts.launch');
// Next, test keys that only work on a selected message.
const message_view_only_keys = '@+>RjJkKsSuvi:GM';
// Check that they do nothing without a selected message
global.current_msg_list.empty = return_true;
assert_unmapped(message_view_only_keys);
global.current_msg_list.empty = return_false;
// Check that they do nothing while in the settings overlay
overlays.settings_open = return_true;
assert_unmapped('@*+->rRjJkKsSuvi:GM');
overlays.settings_open = return_false;
// TODO: Similar check for being in the subs page
assert_mapping('@', 'compose_actions.reply_with_mention');
assert_mapping('+', 'reactions.toggle_emoji_reaction');
assert_mapping('-', 'condense.toggle_collapse');
assert_mapping('r', 'compose_actions.respond_to_message');
assert_mapping('R', 'compose_actions.respond_to_message', true);
assert_mapping('j', 'navigate.down');
assert_mapping('J', 'navigate.page_down');
assert_mapping('k', 'navigate.up');
assert_mapping('K', 'navigate.page_up');
assert_mapping('s', 'narrow.by_recipient');
assert_mapping('S', 'narrow.by_topic');
assert_mapping('u', 'popovers.show_sender_info');
assert_mapping('i', 'popovers.open_message_menu');
assert_mapping(':', 'reactions.open_reactions_popover', true);
assert_mapping('>', 'compose_actions.quote_and_reply');
assert_mapping('e', 'message_edit.start');
overlays.is_active = return_true;
overlays.lightbox_open = return_true;
assert_mapping('v', 'overlays.close_overlay');
overlays.lightbox_open = return_false;
test_normal_typing();
overlays.is_active = return_false;
assert_mapping('v', 'lightbox.show_from_selected_message');
global.emoji_picker.reactions_popped = return_true;
assert_mapping(':', 'emoji_picker.navigate', true);
global.emoji_picker.reactions_popped = return_false;
assert_mapping('G', 'navigate.to_end');
2017-03-23 05:46:19 +01:00
assert_mapping('M', 'muting_ui.toggle_mute');
// Test keys that work when a message is selected and
// also when the message list is empty.
assert_mapping('n', 'narrow.narrow_to_next_topic');
assert_mapping('p', 'narrow.narrow_to_next_pm_string');
global.current_msg_list.empty = return_true;
assert_mapping('n', 'narrow.narrow_to_next_topic');
global.current_msg_list.empty = return_false;
});
2017-03-14 18:29:38 +01:00
run_test('motion_keys', () => {
const codes = {
2017-03-14 18:29:38 +01:00
down_arrow: 40,
end: 35,
home: 36,
left_arrow: 37,
right_arrow: 39,
2017-03-14 18:29:38 +01:00
page_up: 33,
page_down: 34,
spacebar: 32,
up_arrow: 38,
'+': 187,
2017-03-14 18:29:38 +01:00
};
function process(name, shiftKey, ctrlKey) {
const e = {
2017-03-14 18:29:38 +01:00
which: codes[name],
shiftKey: shiftKey,
2017-03-19 19:26:13 +01:00
ctrlKey: ctrlKey,
2017-03-14 18:29:38 +01:00
};
try {
return hotkey.process_keydown(e);
} catch (err) {
// An exception will be thrown here if a different
// function is called than the one declared. Try to
// provide a useful error message.
// add a newline to separate from other console output.
console.log('\nERROR: Mapping for character "' + e.which + '" does not match tests.');
}
2017-03-14 18:29:38 +01:00
}
function assert_unmapped(name) {
assert.equal(process(name), false);
}
function assert_mapping(key_name, func_name, shiftKey, ctrlKey) {
2017-03-14 18:29:38 +01:00
stubbing(func_name, function () {
assert(process(key_name, shiftKey, ctrlKey));
2017-03-14 18:29:38 +01:00
});
}
list_util.inside_list = return_false;
2017-03-14 18:29:38 +01:00
global.current_msg_list.empty = return_true;
overlays.settings_open = return_false;
overlays.streams_open = return_false;
overlays.lightbox_open = return_false;
2017-03-14 18:29:38 +01:00
assert_unmapped('down_arrow');
assert_unmapped('end');
assert_unmapped('home');
assert_unmapped('page_up');
assert_unmapped('page_down');
assert_unmapped('spacebar');
assert_unmapped('up_arrow');
global.list_util.inside_list = return_true;
assert_mapping('up_arrow', 'list_util.go_up');
assert_mapping('down_arrow', 'list_util.go_down');
list_util.inside_list = return_false;
2017-03-14 18:29:38 +01:00
global.current_msg_list.empty = return_false;
assert_mapping('down_arrow', 'navigate.down');
assert_mapping('end', 'navigate.to_end');
assert_mapping('home', 'navigate.to_home');
assert_mapping('left_arrow', 'message_edit.edit_last_sent_message');
assert_mapping('page_up', 'navigate.page_up');
assert_mapping('page_down', 'navigate.page_down');
assert_mapping('spacebar', 'navigate.page_down');
assert_mapping('up_arrow', 'navigate.up');
overlays.info_overlay_open = return_true;
assert_unmapped('down_arrow');
assert_unmapped('up_arrow');
overlays.info_overlay_open = return_false;
overlays.streams_open = return_true;
assert_mapping('up_arrow', 'subs.switch_rows');
assert_mapping('down_arrow', 'subs.switch_rows');
overlays.streams_open = return_false;
overlays.lightbox_open = return_true;
assert_mapping('left_arrow', 'lightbox.prev');
assert_mapping('right_arrow', 'lightbox.next');
overlays.lightbox_open = return_false;
hotkey.in_content_editable_widget = return_true;
assert_unmapped('down_arrow');
assert_unmapped('up_arrow');
hotkey.in_content_editable_widget = return_false;
overlays.settings_open = return_true;
2017-03-14 18:29:38 +01:00
assert_unmapped('end');
assert_unmapped('home');
assert_unmapped('left_arrow');
assert_unmapped('page_up');
assert_unmapped('page_down');
assert_unmapped('spacebar');
overlays.settings_open = return_false;
overlays.is_active = return_true;
overlays.drafts_open = return_true;
assert_mapping('up_arrow', 'drafts.drafts_handle_events');
assert_mapping('down_arrow', 'drafts.drafts_handle_events');
overlays.is_active = return_false;
overlays.drafts_open = return_false;
});