1
/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
3
YUI().use('lazr.autocomplete', 'lazr.testing.runner',
4
'node', 'event', 'console', function(Y) {
6
/*****************************
8
* Helper methods and aliases
11
var Assert = Y.Assert;
13
/* Helper function to clean up a dynamically added widget instance. */
14
function cleanup_widget(widget) {
15
// Nuke the boundingBox, but only if we've touched the DOM.
16
if (widget.get('rendered')) {
17
var bb = widget.get('boundingBox');
18
bb.get('parentNode').removeChild(bb);
20
// Kill the widget itself.
24
/* A helper to create a simple text input box */
25
function make_input(value) {
26
var input = document.createElement('input');
27
input.setAttribute('type', 'text');
28
input.setAttribute('value', value || '');
29
Y.one('body').appendChild(input);
33
/* A helper to destroy a generic input: make_input()'s inverse */
34
function kill_input(input) {
35
Y.one('body').removeChild(input);
39
/****************************
45
var suite = new Y.Test.Suite('autocomplete Test Suite');
48
suite.add(new Y.Test.Case({
50
name:'test widget setup',
53
this.input = make_input();
56
tearDown: function() {
57
kill_input(this.input);
60
test_widget_starts_hidden: function() {
61
var autocomp = new Y.lazr.AutoComplete({ input: this.input });
64
autocomp.get('visible'),
65
"The widget should start out hidden.");
70
suite.add(new Y.Test.Case({
72
name:'test display of matching results',
75
this.input = make_input();
76
this.autocomp = new Y.lazr.AutoComplete({
81
tearDown: function() {
82
cleanup_widget(this.autocomp);
83
kill_input(this.input);
86
/* A helper to option the completions list for a given input string. */
87
complete_input: function(value) {
88
this.input.value = value;
89
var last_charcode = value.charCodeAt(value.length - 1);
90
Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
93
/* Extract the matching text from the widget's autocompletion list. */
94
get_completions: function() {
95
if (!this.autocomp.get('rendered')) {
96
Y.fail("Tried find matches for an unrendered widget.");
104
.each(function(item) {
105
matches.push(item.get('text'));
110
test_autocomplete_is_visible_if_results_match: function() {
111
this.autocomp.set('data', ['aaa']);
112
this.autocomp.render();
114
// We want to match the one and only data set element.
115
this.complete_input('aa');
117
this.autocomp.get('visible'),
118
"The widget should be visible if matching input was found.");
121
test_autocomplete_is_hidden_if_no_query_is_given: function() {
122
this.autocomp.set('data', ['aaa']);
123
this.autocomp.render();
125
// We want to simulate an empty input field, but some action triggers
127
this.complete_input('');
129
this.autocomp.get('visible'),
130
"The widget should be hidden if the input field is empty.");
133
test_autocomplete_is_hidden_if_results_do_not_match: function() {
134
this.autocomp.set('data', ['bbb']);
135
this.autocomp.render();
137
if (this.autocomp.get('visible')) {
138
Y.fail("The autocomplete widget should start out hidden.");
142
// 'aa' shouldn't match any of the data.
143
this.complete_input('aa');
145
this.autocomp.get('visible'),
146
"The widget should be hidden if the query doesn't match any " +
147
"possible completions.");
150
test_display_should_contain_all_matches: function() {
156
this.autocomp.set('data', data);
157
this.autocomp.render();
159
// Trigger autocompletion, should match all data items.
160
this.complete_input('aa');
162
// Grab the now-open menu
163
var option_list = Y.one('.yui3-autocomplete-list');
164
Assert.isObject(option_list,
165
"The list of completion options should be open.");
167
Y.ArrayAssert.itemsAreEqual(
168
this.get_completions(),
170
"Every autocomplete item should be present in the available " +
174
test_display_is_updated_with_new_completions: function() {
175
// Create two pieces of data, each narrower than the other.
176
this.autocomp.set('data', ['aaa', 'aab']);
177
this.autocomp.render();
179
// Trigger autocompletion for the loosest matches
180
this.complete_input('aa');
181
// Complete the narrower set
182
this.complete_input('aaa');
184
var completions = this.get_completions();
186
Y.ArrayAssert.itemsAreEqual(
189
"'aaa' should be the data item displayed after narrowing the " +
190
"search with the query 'aaa'.");
193
test_matching_text_in_item_is_marked: function() {
194
this.autocomp.set('data', ['aaa']);
195
this.autocomp.render();
197
// Display the matching input.
199
this.complete_input(query);
201
// Grab the matching item
202
var matching_text = this.autocomp
204
.one('.item .matching-text');
206
Assert.isNotNull(matching_text,
207
"Some of the matching item's text should be marked as matching.");
211
matching_text.get('text'),
212
"The matching text should be the same as the query text.");
215
test_escape_key_should_close_completions_list: function() {
216
this.autocomp.set('data', ['aaa']);
217
this.autocomp.render();
219
// Open the completions list
220
this.complete_input('aa');
222
// Hit the escape key to close the list
223
Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
226
this.autocomp.get('visible'),
227
"The list of completions should be closed after pressing the " +
232
suite.add(new Y.Test.Case({
234
name:'test result text marking method',
236
test_match_at_beginning_should_be_marked: function() {
237
var autocomp = new Y.lazr.AutoComplete();
238
var marked_text = autocomp.markMatchingText('aabb', 'aa', 0);
241
'<span class="matching-text">aa</span>bb',
243
"The text at the beginning of the result should have been " +
247
test_match_in_middle_should_be_marked: function() {
248
var autocomp = new Y.lazr.AutoComplete();
249
var marked_text = autocomp.markMatchingText('baab', 'aa', 1);
252
'b<span class="matching-text">aa</span>b',
254
"The text in the middle of the result should have been " +
258
test_match_at_end_should_be_marked: function() {
259
var autocomp = new Y.lazr.AutoComplete();
260
var marked_text = autocomp.markMatchingText('bbaa', 'aa', 2);
263
'bb<span class="matching-text">aa</span>',
265
"The text at the end of the result should have been " +
271
suite.add(new Y.Test.Case({
273
name:'test query parsing',
276
this.autocomplete = new Y.lazr.AutoComplete({
281
test_space_for_delimiter: function() {
284
this.autocomplete.parseQuery('a b').text,
285
"Input should be split around the 'space' character.");
287
this.autocomplete.parseQuery(' '),
288
"Space for input and delimiter should not parse.");
291
test_parsed_query_is_stripped_of_leading_whitespace: function() {
292
this.autocomplete.set('delimiter', ',');
296
this.autocomplete.parseQuery(' a').text,
297
"Leading whitespace at the start of the input string should " +
302
this.autocomplete.parseQuery('a, b').text,
303
"Leading whitespace between the last separator and the current " +
304
"query should be stripped.");
307
test_query_is_taken_from_middle_of_input: function() {
308
// Pick a caret position that is in the middle of the second result.
309
var input = "aaa bbb ccc";
314
this.autocomplete.parseQuery(input, caret).text,
315
"The current query should be picked out of the middle of the " +
316
"text input if the caret has been positioned there.");
319
test_query_is_taken_from_beginning_of_input: function() {
320
// Pick a caret position that is in the first input's query
321
var input = "aaa bbb";
326
this.autocomplete.parseQuery(input, caret).text,
327
"The first block of text should become the current query if " +
328
"the caret is positioned within it.");
332
suite.add(new Y.Test.Case({
334
name:'test results matching algorithm',
336
/* A helper function to determine if two match result items are equal */
337
matches_are_equal: function(a, b) {
338
if (typeof a == 'undefined') {
339
Assert.fail("Match set 'a' is of type 'undefined'!");
341
if (typeof b == 'undefined') {
342
Assert.fail("Match set 'b' is of type 'undefined'!");
344
return (a.text == b.text) && (a.offset == b.offset);
347
test_no_matches_returns_an_empty_array: function() {
348
var autocomplete = new Y.lazr.AutoComplete({
352
var matches = autocomplete.findMatches('aa');
353
Y.ArrayAssert.isEmpty(matches,
354
"No data should have matched the query 'aa'");
357
test_match_last_item: function() {
358
var autocomplete = new Y.lazr.AutoComplete({
366
var matches = autocomplete.findMatches('aa');
368
Y.ArrayAssert.itemsAreEquivalent(
369
[{text: 'aaa', offset: 0}],
371
this.matches_are_equal,
372
"One row should have matched the query 'aa'.");
375
test_match_ordering: function() {
376
// Matches, in reverse order.
377
var autocomplete = new Y.lazr.AutoComplete({
385
var matches = autocomplete.findMatches('aa');
387
Y.ArrayAssert.itemsAreEquivalent(
388
[{text: 'aabb', offset: 0},
389
{text: 'baab', offset: 1},
390
{text: 'bbaa', offset: 2}],
392
this.matches_are_equal,
393
"The match array should have all of it's keys in order.");
396
test_mixed_case_text_matches: function() {
397
var autocomplete = new Y.lazr.AutoComplete({
401
var matches = autocomplete.findMatches('b');
403
Y.ArrayAssert.itemsAreEquivalent(
404
[{text:'aBc', offset: 1}],
406
this.matches_are_equal,
407
"The match algorithm should be case insensitive.");
410
test_mixed_case_matches_come_in_stable_order: function() {
411
// Data with the mixed-case coming first in order.
412
var autocomplete = new Y.lazr.AutoComplete({
413
data: ['aBc', 'aaa', 'abc']
416
var matches = autocomplete.findMatches('b');
418
Y.ArrayAssert.itemsAreEquivalent(
419
[{text: 'aBc', offset: 1},
420
{text: 'abc', offset: 1}],
422
this.matches_are_equal,
423
"Mixed-case matches should arrive in stable order.");
428
suite.add(new Y.Test.Case({
430
name:'test selecting results',
433
this.input = make_input();
434
this.autocomp = new Y.lazr.AutoComplete({
437
this.autocomp.render();
440
tearDown: function() {
441
cleanup_widget(this.autocomp);
442
kill_input(this.input);
445
/* A helper to option the completions list for a given input string. */
446
complete_input: function(value) {
447
this.input.value = value;
448
var last_charcode = value.charCodeAt(value.length - 1);
449
Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
452
/* A helper to select the selected completion result with the Tab key. */
453
press_selection_key: function() {
454
Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
457
test_pressing_enter_completes_current_input: function() {
458
this.autocomp.set('data', ['aaaa', 'aabb']);
460
// Open the completion options
461
this.complete_input('aa');
464
Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
469
"The first completion should have been appended to the input's " +
470
"value after pressing the 'Enter' key.");
473
test_pressing_tab_completes_current_input: function() {
474
this.autocomp.set('data', ['aaaa', 'aabb']);
476
// Open the completion options
477
this.complete_input('aa');
480
Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
485
"The first completion should have been appended to the input's " +
486
"value after pressing the 'Enter' key.");
489
test_clicking_on_first_result_completes_input: function() {
490
this.autocomp.set('data', ['aaaa', 'aabb']);
491
this.complete_input('aa');
493
// Click on the first displayed result
494
var options = this.autocomp.get('contentBox').all('.item');
495
var first_item = Y.Node.getDOMNode(options.item(0));
496
Y.Event.simulate(first_item, 'click');
501
"The first completion should have been appended to the input's " +
502
"value after clicking it's list node.");
505
test_selecting_results_hides_completion_list: function() {
506
this.autocomp.set('data', 'aaa');
507
this.complete_input('a');
508
this.press_selection_key();
511
this.autocomp.get('visible'),
512
"The completion list should be hidden after a result is " +
516
test_completed_input_replaces_current_input: function() {
517
this.autocomp.set('data', ['abba']);
519
// Match the one and only result, but match the second character in
520
// it. Throw in some pre-existing user input just to be sure things
522
this.complete_input('xxx b');
523
this.press_selection_key();
528
"The user's current query should have been replaced with the " +
532
test_completed_input_has_delimiter_appended_to_it: function() {
534
this.autocomp.set('data', ['aaaa']);
535
this.autocomp.set('delimiter', delimiter);
537
this.complete_input('a');
538
this.press_selection_key();
542
this.input.value.charAt(this.input.value.length - 1),
543
"The last character of the input should be the current " +
547
test_down_arrow_selects_second_result_in_list: function() {
548
this.autocomp.set('data', ['first_item', 'second_item']);
550
// Match the first result. It should be selected by default.
551
this.complete_input('item');
553
// Simulate pressing the down arrow key.
554
Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
556
// Now, select the second result.
557
this.press_selection_key();
562
"Pressing the down-arrow key should select the second option " +
563
"in the completions list.");
567
Y.lazr.testing.Runner.add(suite);
568
Y.lazr.testing.Runner.run();