node tests: Add coverage to composebox_typeahead.js.

This commit is contained in:
Yago González 2017-06-26 22:39:04 +02:00 committed by showell
parent 9be3ff2b3f
commit 00a7f89ba9
2 changed files with 672 additions and 6 deletions

View File

@ -1,21 +1,62 @@
var ct = require('js/composebox_typeahead.js'); var ct = require('js/composebox_typeahead.js');
var emoji_list = [{emoji_name: "tada", emoji_url: "TBD"}, var emoji_stadium = {
{emoji_name: "moneybags", emoji_url: "TBD"}]; emoji_name: 'stadium',
var stream_list = ['Denmark', 'Sweden']; emoji_url: 'TBD',
};
var emoji_tada = {
emoji_name: 'tada',
emoji_url: 'TBD',
};
var emoji_moneybag = {
emoji_name: 'moneybag',
emoji_url: 'TBD',
};
var emoji_list = [ emoji_tada, emoji_moneybag, emoji_stadium ];
var stream_list = ['Denmark', 'Sweden', 'The Netherlands'];
var sweden_stream = {
name: 'Sweden',
description: 'Cold, mountains and home decor.',
stream_id: 1,
};
var denmark_stream = {
name: 'Denmark',
description: 'Vikings and boats, in a cold weather.',
stream_id: 2,
};
set_global('$', global.make_zjquery());
set_global('page_params', {});
set_global('channel', {});
set_global('emoji', {emojis: emoji_list}); set_global('emoji', {emojis: emoji_list});
set_global('stream_data', {subscribed_subs: function () {
return stream_list;
}});
set_global('pygments_data', {langs: set_global('pygments_data', {langs:
{python: 0, javscript: 1, html: 2, css: 3}, {python: 0, javscript: 1, html: 2, css: 3},
}); });
add_dependencies({ add_dependencies({
Handlebars: 'handlebars',
compose_state: 'js/compose_state.js',
people: 'js/people.js', people: 'js/people.js',
stream_data: 'js/stream_data',
templates: 'js/templates',
typeahead_helper: 'js/typeahead_helper.js',
ui_util: 'js/ui_util.js',
util: 'js/util.js',
}); });
global.compile_template('typeahead_list_item');
stream_data.subscribed_subs = function () {
return stream_list;
};
stream_data.subscribed_streams = function () {
return stream_list;
};
var othello = { var othello = {
email: 'othello@zulip.com', email: 'othello@zulip.com',
user_id: 101, user_id: 101,
@ -49,6 +90,584 @@ global.people.add(deactivated_user);
); );
}()); }());
(function test_topics_seen_for() {
// Test when the stream doesn't exist (there are no topics)
assert.deepEqual(ct.topics_seen_for('non-existing-stream'), []);
}());
(function test_content_typeahead_selected() {
var fake_this = {
query: '',
$element: {},
};
var caret_called1 = false;
var caret_called2 = false;
fake_this.$element.caret = function (arg1, arg2) {
if (arguments.length === 0) { // .caret() used in split_at_cursor
caret_called1 = true;
return fake_this.query.length;
}
// .caret() used in setTimeout
assert.equal(arg1, arg2);
caret_called2 = true;
};
var autosize_called = false;
set_global('compose_ui', {
autosize_textarea: function () {
autosize_called = true;
},
});
var set_timeout_called = false;
global.patch_builtin('setTimeout', function (f, time) {
f();
assert.equal(time, 0);
set_timeout_called = true;
});
set_global('document', 'document-stub');
// emoji
fake_this.completing = 'emoji';
fake_this.query = ':octo';
var item = {
emoji_name: 'octopus',
};
var actual_value = ct.content_typeahead_selected.call(fake_this, item);
var expected_value = ':octopus: ';
assert.equal(actual_value, expected_value);
fake_this.query = ' :octo';
actual_value = ct.content_typeahead_selected.call(fake_this, item);
expected_value = ' :octopus: ';
assert.equal(actual_value, expected_value);
fake_this.query = '{:octo';
actual_value = ct.content_typeahead_selected.call(fake_this, item);
expected_value = '{ :octopus: ';
assert.equal(actual_value, expected_value);
// mention
fake_this.completing = 'mention';
var document_stub_trigger1_called = false;
$('document-stub').trigger = function (event, params) {
assert.equal(event, 'usermention_completed.zulip');
assert.deepEqual(params, { mentioned: othello });
document_stub_trigger1_called = true;
};
fake_this.query = '@oth';
fake_this.token = 'oth';
actual_value = ct.content_typeahead_selected.call(fake_this, othello);
expected_value = '@**Othello, the Moor of Venice** ';
assert.equal(actual_value, expected_value);
// stream
fake_this.completing = 'stream';
var document_stub_trigger2_called = false;
$('document-stub').trigger = function (event, params) {
assert.equal(event, 'streamname_completed.zulip');
assert.deepEqual(params, { stream: sweden_stream });
document_stub_trigger2_called = true;
};
fake_this.query = '#swed';
fake_this.token = 'swed';
actual_value = ct.content_typeahead_selected.call(fake_this, sweden_stream);
expected_value = '#**Sweden** ';
assert.equal(actual_value, expected_value);
// syntax
fake_this.completing = 'syntax';
fake_this.query = '~~~p';
fake_this.token = 'p';
actual_value = ct.content_typeahead_selected.call(fake_this, 'python');
expected_value = '~~~python\n\n~~~';
assert.equal(actual_value, expected_value);
fake_this.query = '```p';
fake_this.token = 'p';
actual_value = ct.content_typeahead_selected.call(fake_this, 'python');
expected_value = '```python\n\n```';
assert.equal(actual_value, expected_value);
fake_this.completing = 'something-else';
fake_this.query = 'foo';
actual_value = ct.content_typeahead_selected.call(fake_this, {});
expected_value = fake_this.query;
assert.equal(actual_value, expected_value);
assert(caret_called1);
assert(caret_called2);
assert(autosize_called);
assert(set_timeout_called);
assert(document_stub_trigger1_called);
assert(document_stub_trigger2_called);
}());
(function test_initialize() {
var stream_typeahead_called = false;
$('#stream').typeahead = function (options) {
// options.source()
//
// We'll search through the streams in stream_list for the streams
// typeahead.
var actual_value = options.source();
var expected_value = stream_list;
assert.deepEqual(actual_value, expected_value);
// options.highlighter()
options.query = 'De'; // Beginning of "Denmark", one of the streams
// provided in stream_list through .source().
actual_value = options.highlighter('Denmark');
expected_value = '<strong>Denmark</strong>';
assert.equal(actual_value, expected_value);
options.query = 'the n';
actual_value = options.highlighter('The Netherlands');
expected_value = '<strong>The Netherlands</strong>';
assert.equal(actual_value, expected_value);
// options.matcher()
options.query = 'de';
assert.equal(options.matcher('Denmark'), true);
assert.equal(options.matcher('Sweden'), false);
options.query = 'De';
assert.equal(options.matcher('Denmark'), true);
assert.equal(options.matcher('Sweden'), false);
options.query = 'the ';
assert.equal(options.matcher('The Netherlands'), true);
assert.equal(options.matcher('Sweden'), false);
stream_typeahead_called = true;
};
var subject_typeahead_called = false;
$('#subject').typeahead = function (options) {
// options.source()
ct.add_topic('Sweden', 'furniture');
ct.add_topic('Sweden', 'kronor');
ct.add_topic('Sweden', 'ice');
ct.add_topic('Sweden', 'more ice');
ct.add_topic('Sweden', 'even more ice');
ct.add_topic('Sweden', '<&>');
var topics = [ '<&>', 'even more ice', 'furniture', 'ice', 'kronor', 'more ice' ];
$('#stream').val('Sweden');
var actual_value = options.source();
// Topics should be sorted alphabetically, not by addition order.
var expected_value = topics;
assert.deepEqual(actual_value, expected_value);
// options.highlighter()
options.query = 'Kro';
actual_value = options.highlighter('kronor');
expected_value = '<strong>kronor</strong>';
assert.equal(actual_value, expected_value);
// Highlighted content should be escaped.
options.query = '<';
actual_value = options.highlighter('<&>');
expected_value = '<strong>&lt;&amp;&gt;</strong>';
assert.equal(actual_value, expected_value);
options.query = 'even m';
actual_value = options.highlighter('even more ice');
expected_value = '<strong>even more ice</strong>';
assert.equal(actual_value, expected_value);
// options.sorter()
//
// Notice that alphabetical sorting isn't managed by this sorter,
// it is a result of the topics already being sorted after adding
// them with ct.add_topic().
options.query = 'furniture';
actual_value = options.sorter([ 'furniture' ]);
expected_value = [ 'furniture' ];
assert.deepEqual(actual_value, expected_value);
// A literal match at the beginning of an element puts it at the top.
options.query = 'ice';
actual_value = options.sorter([ 'even more ice', 'ice', 'more ice' ]);
expected_value = [ 'ice', 'even more ice', 'more ice' ];
assert.deepEqual(actual_value, expected_value);
// The sorter should return the query as the first element if there
// isn't a topic with such name.
// This only happens if typeahead is providing other suggestions.
options.query = 'e'; // Letter present in "furniture" and "ice"
actual_value = options.sorter([ 'furniture', 'ice' ]);
expected_value = [ 'e', 'furniture', 'ice' ];
assert.deepEqual(actual_value, expected_value);
// Don't make any suggestions if this query doesn't match any
// existing topic.
options.query = 'non-existing-topic';
actual_value = options.sorter([]);
expected_value = [];
assert.deepEqual(actual_value, expected_value);
subject_typeahead_called = true;
};
var pm_recipient_typeahead_called = false;
$('#private_message_recipient').typeahead = function (options) {
// options.source()
//
// This should match the users added at the beginning of this test file.
var actual_value = options.source();
var expected_value = [ othello, cordelia, deactivated_user ];
assert.deepEqual(actual_value, expected_value);
// options.highlighter()
//
// Even though the items passed to .highlighter() are the full
// objects of the users matching the query, it only returns the
// HTML string with the "User_name <email>" format, with the
// corresponding parts in bold.
options.query = 'oth';
actual_value = options.highlighter(othello);
expected_value = '<strong>Othello, the Moor of Venice</strong>&nbsp;&nbsp;\n<small class="autocomplete_secondary">othello@zulip.com</small>';
assert.equal(actual_value, expected_value);
options.query = 'Lear';
actual_value = options.highlighter(cordelia);
expected_value = '<strong>Cordelia Lear</strong>&nbsp;&nbsp;\n<small class="autocomplete_secondary">cordelia@zulip.com</small>';
assert.equal(actual_value, expected_value);
options.query = 'othello@zulip.com, co';
actual_value = options.highlighter(cordelia);
expected_value = '<strong>Cordelia Lear</strong>&nbsp;&nbsp;\n<small class="autocomplete_secondary">cordelia@zulip.com</small>';
assert.equal(actual_value, expected_value);
// options.matcher()
options.query = 'el'; // Matches both "othELlo" and "cordELia"
assert.equal(options.matcher(othello), true);
assert.equal(options.matcher(cordelia), true);
assert.equal(options.matcher(deactivated_user), false);
// Othello is already filled in, now typeahead makes suggestions for
// the value after the comma.
options.query = 'othello@zulip.com, cor';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), true);
assert.equal(options.matcher(deactivated_user), false);
// No suggestions are made if the query is just a comma.
options.query = ',';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false);
assert.equal(options.matcher(deactivated_user), false);
options.query = 'bender'; // Doesn't exist
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false);
assert.equal(options.matcher(deactivated_user), false);
// Don't make suggestions if the last name only has whitespaces
// (we're between typing names).
options.query = 'othello@zulip.com, ';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false);
assert.equal(options.matcher(deactivated_user), false);
options.query = 'othello@zulip.com,, , cord';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), true);
assert.equal(options.matcher(deactivated_user), false);
// If the user is already in the list, typeahead doesn't include it
// again.
options.query = 'cordelia@zulip.com, cord';
assert.equal(options.matcher(othello), false);
assert.equal(options.matcher(cordelia), false);
assert.equal(options.matcher(deactivated_user), false);
// options.sorter()
//
// The sorter's output has the items that match the query from the
// beginning first, and then the rest of them in REVERSE order of
// the input.
options.query = 'othello';
actual_value = options.sorter([ othello ]);
expected_value = [ othello ];
assert.deepEqual(actual_value, expected_value);
// A literal match at the beginning of an element puts it at the top.
options.query = 'co'; // Matches everything ("x@zulip.COm")
actual_value = options.sorter([ othello, deactivated_user, cordelia ]);
expected_value = [ cordelia, deactivated_user, othello ];
assert.deepEqual(actual_value, expected_value);
options.query = 'non-existing-user';
actual_value = options.sorter([]);
expected_value = [];
assert.deepEqual(actual_value, expected_value);
// options.updater()
options.query = 'othello';
actual_value = options.updater(othello);
expected_value = 'othello@zulip.com, ';
assert.equal(actual_value, expected_value);
options.query = 'othello@zulip.com, cor';
actual_value = options.updater(cordelia);
expected_value = 'othello@zulip.com, cordelia@zulip.com, ';
assert.equal(actual_value, expected_value);
var click_event = { type: 'click' };
options.query = 'othello';
// Focus lost (caused by the click event in the typeahead list)
$('#private_message_recipient').blur();
actual_value = options.updater(othello, click_event);
expected_value = 'othello@zulip.com, ';
assert.equal(actual_value, expected_value);
// Check that after the click event #private_message_recipient is
// focused.
assert.equal($('#private_message_recipient').is_focused(), true);
pm_recipient_typeahead_called = true;
};
var new_message_content_typeahead_called = false;
$('#new_message_content').typeahead = function (options) {
// options.source()
//
// For now we only test that compose_contents_begins_typeahead has been
// properly set as the .source(). All its features are tested later on
// in test_begins_typeahead().
var fake_this = {
$element: {},
};
var caret_called = false;
fake_this.$element.caret = function () { caret_called = true; };
fake_this.options = options;
var actual_value = options.source.call(fake_this, 'test #s');
var expected_value = stream_list;
assert.deepEqual(actual_value, expected_value);
assert(caret_called);
// options.highlighter()
//
// Again, here we only verify that the highlighter has been set to
// content_highlighter.
fake_this = { completing: 'mention', token: 'othello' };
actual_value = options.highlighter.call(fake_this, othello);
expected_value = '<strong>Othello, the Moor of Venice</strong>&nbsp;&nbsp;\n<small class="autocomplete_secondary">othello@zulip.com</small>';
assert.equal(actual_value, expected_value);
// options.matcher()
fake_this = { completing: 'emoji', token: 'ta' };
assert.equal(options.matcher.call(fake_this, emoji_tada), true);
assert.equal(options.matcher.call(fake_this, emoji_moneybag), false);
fake_this = { completing: 'mention', token: 'Cord' };
assert.equal(options.matcher.call(fake_this, cordelia), true);
assert.equal(options.matcher.call(fake_this, othello), false);
fake_this = { completing: 'stream', token: 'swed' };
assert.equal(options.matcher.call(fake_this, sweden_stream), true);
assert.equal(options.matcher.call(fake_this, denmark_stream), false);
fake_this = { completing: 'syntax', token: 'py' };
assert.equal(options.matcher.call(fake_this, 'python'), true);
assert.equal(options.matcher.call(fake_this, 'javascript'), false);
fake_this = { completing: 'non-existing-completion' };
assert.equal(options.matcher.call(fake_this), undefined);
// options.sorter()
fake_this = { completing: 'emoji', token: 'ta' };
actual_value = options.sorter.call(fake_this, [ emoji_stadium, emoji_tada ]);
expected_value = [ emoji_tada, emoji_stadium ];
assert.deepEqual(actual_value, expected_value);
fake_this = { completing: 'mention', token: 'co' };
actual_value = options.sorter.call(fake_this, [ othello, cordelia ]);
expected_value = [ cordelia, othello ];
assert.deepEqual(actual_value, expected_value);
fake_this = { completing: 'stream', token: 'de' };
actual_value = options.sorter.call(fake_this, [ sweden_stream, denmark_stream ]);
expected_value = [ denmark_stream, sweden_stream ];
assert.deepEqual(actual_value, expected_value);
// Matches in the descriptions affect the order as well.
// Testing "co" for "cold", in both streams' description. It's at the
// beginning of Sweden's description, so that one should go first.
fake_this = { completing: 'stream', token: 'co' };
actual_value = options.sorter.call(fake_this, [ denmark_stream, sweden_stream ]);
expected_value = [ sweden_stream, denmark_stream ];
assert.deepEqual(actual_value, expected_value);
fake_this = { completing: 'syntax', token: 'ap' };
actual_value = options.sorter.call(fake_this, [ 'abap', 'applescript' ]);
expected_value = [ 'applescript', 'abap' ];
assert.deepEqual(actual_value, expected_value);
fake_this = { completing: 'non-existing-completion' };
assert.equal(options.sorter.call(fake_this), undefined);
new_message_content_typeahead_called = true;
};
var pm_recipient_blur_called = false;
var old_pm_recipient_blur = $('#private_message_recipient').blur;
$('#private_message_recipient').blur = function (handler) {
if (handler) { // The blur handler is being set.
this.val('othello@zulip.com, ');
handler.call(this);
var actual_value = this.val();
var expected_value = 'othello@zulip.com';
assert.equal(actual_value, expected_value);
} else { // The element is simply losing the focus.
old_pm_recipient_blur();
}
pm_recipient_blur_called = true;
};
page_params.enter_sends = false; // We manually specify it the first
// time because the click_func
// doesn't exist yet.
var noop = function () {};
$("#stream").select(noop);
$("#subject").select(noop);
$("#private_message_recipient").select(noop);
ct.initialize();
// handle_keydown()
var event = {
keyCode: 13,
target: {
id: 'stream',
},
preventDefault: noop,
};
$('#subject').data = function () {
return { typeahead: { shown: true }};
};
$('form#send_message_form').keydown(event);
var stub_typeahead_hidden = function () {
return { typeahead: { shown: false }};
};
$('#subject').data = stub_typeahead_hidden;
$('#stream').data = stub_typeahead_hidden;
$('#private_message_recipient').data = stub_typeahead_hidden;
$('#new_message_content').data = stub_typeahead_hidden;
$('form#send_message_form').keydown(event);
event.keyCode = undefined;
event.which = 9;
event.shiftKey = false;
event.target.id = 'subject';
$('form#send_message_form').keydown(event);
event.target.id = 'new_message_content';
$('form#send_message_form').keydown(event);
event.target.id = 'some_non_existing_id';
$('form#send_message_form').keydown(event);
event.keyCode = 13;
event.target.id = 'subject';
$('form#send_message_form').keydown(event);
event.target.id = 'new_message_content';
page_params.enter_sends = false;
event.metaKey = true;
var compose_finish_called = false;
set_global('compose', {
finish: function () {
compose_finish_called = true;
},
});
$('form#send_message_form').keydown(event);
assert(compose_finish_called);
event.metaKey = false;
event.ctrlKey = true;
$('form#send_message_form').keydown(event);
page_params.enter_sends = true;
event.ctrlKey = false;
event.altKey = true;
$('form#send_message_form').keydown(event);
event.metaKey = true;
$('form#send_message_form').keydown(event);
event.target.id = 'private_message_recipient';
$('form#send_message_form').keydown(event);
event.keyCode = 42;
$('form#send_message_form').keydown(event);
// handle_keyup()
event = {
keyCode: 13,
target: {
id: 'stream',
},
preventDefault: noop,
};
// We execute .keydown() in order to make nextFocus !== false
$('#subject').data = function () {
return { typeahead: { shown: true }};
};
$('form#send_message_form').keydown(event);
$('form#send_message_form').keyup(event);
event.keyCode = undefined;
event.which = 9;
event.shiftKey = false;
$('form#send_message_form').keyup(event);
event.keyCode = 42;
$('form#send_message_form').keyup(event);
// select_on_focus()
var focus_handler_called = false;
var stream_one_called = false;
$('#stream').focus = function (f) {
// This .one() function emulates the possible infinite recursion that
// in_handler tries to avoid.
$('#stream').one = function (event, handler) {
handler({ preventDefault: noop });
f(); // This time in_handler will already be true.
stream_one_called = true;
};
f(); // Here in_handler is false.
focus_handler_called = true;
};
$("#compose-send-button").fadeOut = noop;
$("#compose-send-button").fadeIn = noop;
var channel_post_called = false;
global.channel.post = function (params) {
assert.equal(params.url, '/json/users/me/enter-sends');
assert.equal(params.idempotent, true);
assert.deepEqual(params.data, {enter_sends: page_params.enter_sends});
channel_post_called = true;
};
$('#enter_sends').is = function () { return false; };
$('#enter_sends').click();
// Now we re-run both .initialize() and the click handler, this time
// with enter_sends: page_params.enter_sends being true
$('#enter_sends').is = function () { return true; };
$('#enter_sends').click();
ct.initialize();
// Now let's make sure that all the stub functions have been called
// during the initialization.
assert(stream_typeahead_called);
assert(subject_typeahead_called);
assert(pm_recipient_typeahead_called);
assert(pm_recipient_blur_called);
assert(channel_post_called);
assert(new_message_content_typeahead_called);
assert(focus_handler_called);
assert(stream_one_called);
}());
(function test_begins_typeahead() { (function test_begins_typeahead() {
// Stub out split_at_cursor that uses $(':focus') // Stub out split_at_cursor that uses $(':focus')
ct.split_at_cursor = function (word) { return [word, '']; }; ct.split_at_cursor = function (word) { return [word, '']; };
@ -171,9 +790,55 @@ global.people.add(deactivated_user);
assert.equal(ct.tokenize_compose_str("foo ```p"), ""); assert.equal(ct.tokenize_compose_str("foo ```p"), "");
assert.equal(ct.tokenize_compose_str("``` py"), "``` py"); assert.equal(ct.tokenize_compose_str("``` py"), "``` py");
assert.equal(ct.tokenize_compose_str("foo``bar ~~~ py"), ""); assert.equal(ct.tokenize_compose_str("foo``bar ~~~ py"), "");
assert.equal(ct.tokenize_compose_str("foo ~~~why = why_not\n~~~"), "~~~");
// The following cases are kinda judgment calls... // The following cases are kinda judgment calls...
assert.equal(ct.tokenize_compose_str( assert.equal(ct.tokenize_compose_str(
"foo @toomanycharactersisridiculoustocomplete"), ""); "foo @toomanycharactersisridiculoustocomplete"), "");
assert.equal(ct.tokenize_compose_str("foo #streams@foo"), "#streams@foo"); assert.equal(ct.tokenize_compose_str("foo #streams@foo"), "#streams@foo");
}()); }());
(function test_content_highlighter() {
var fake_this = { completing: 'emoji' };
var item = { emoji_name: 'person shrugging', emoji_url: '¯\_(ツ)_/¯' };
var th_render_typeahead_item_called = false;
typeahead_helper.render_typeahead_item = function (item) {
assert.equal(item.primary, 'person shrugging');
assert.equal(item.img_src, '¯\_(ツ)_/¯');
th_render_typeahead_item_called = true;
};
ct.content_highlighter.call(fake_this, item);
fake_this = { completing: 'mention' };
var th_render_person_called = false;
typeahead_helper.render_person = function (person) {
assert.deepEqual(person, othello);
th_render_person_called = true;
};
ct.content_highlighter.call(fake_this, othello);
fake_this = { completing: 'stream' };
var th_render_stream_called = false;
typeahead_helper.render_stream = function (stream) {
assert.deepEqual(stream, denmark_stream);
th_render_stream_called = true;
};
ct.content_highlighter.call(fake_this, denmark_stream);
fake_this = { completing: 'syntax' };
th_render_typeahead_item_called = false;
typeahead_helper.render_typeahead_item = function (item) {
assert.deepEqual(item, { primary: 'py' });
th_render_typeahead_item_called = true;
};
ct.content_highlighter.call(fake_this, 'py' );
fake_this = { completing: 'something-else' };
assert(!ct.content_highlighter.call(fake_this));
// Verify that all stub functions have been called.
assert(th_render_typeahead_item_called);
assert(th_render_person_called);
assert(th_render_stream_called);
assert(th_render_typeahead_item_called);
}());

View File

@ -29,6 +29,7 @@ enforce_fully_covered = {
'static/js/common.js', 'static/js/common.js',
'static/js/compose_state.js', 'static/js/compose_state.js',
'static/js/compose_ui.js', 'static/js/compose_ui.js',
'static/js/composebox_typeahead.js',
'static/js/dict.js', 'static/js/dict.js',
'static/js/emoji.js', 'static/js/emoji.js',
'static/js/filter.js', 'static/js/filter.js',