~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/app/javascript/lazr/autocomplete/tests/autocomplete.js

[r=deryck][bug=803954] Bring lazr-js source into lp tree and package
        yui as a dependency

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
2
 
 
3
YUI().use('lazr.autocomplete', 'lazr.testing.runner',
 
4
          'node', 'event', 'console', function(Y) {
 
5
 
 
6
/*****************************
 
7
 *
 
8
 *  Helper methods and aliases
 
9
 *
 
10
 */
 
11
var Assert = Y.Assert;
 
12
 
 
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);
 
19
    }
 
20
    // Kill the widget itself.
 
21
    widget.destroy();
 
22
}
 
23
 
 
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);
 
30
    return input;
 
31
}
 
32
 
 
33
/* A helper to destroy a generic input: make_input()'s inverse */
 
34
function kill_input(input) {
 
35
    Y.one('body').removeChild(input);
 
36
}
 
37
 
 
38
 
 
39
/****************************
 
40
 *
 
41
 *  Tests
 
42
 *
 
43
 */
 
44
 
 
45
var suite = new Y.Test.Suite('autocomplete Test Suite');
 
46
 
 
47
 
 
48
suite.add(new Y.Test.Case({
 
49
 
 
50
    name:'test widget setup',
 
51
 
 
52
    setUp: function() {
 
53
        this.input = make_input();
 
54
    },
 
55
 
 
56
    tearDown: function() {
 
57
        kill_input(this.input);
 
58
    },
 
59
 
 
60
    test_widget_starts_hidden: function() {
 
61
        var autocomp = new Y.lazr.AutoComplete({ input: this.input });
 
62
        autocomp.render();
 
63
        Assert.isFalse(
 
64
            autocomp.get('visible'),
 
65
            "The widget should start out hidden.");
 
66
    }
 
67
}));
 
68
 
 
69
 
 
70
suite.add(new Y.Test.Case({
 
71
 
 
72
    name:'test display of matching results',
 
73
 
 
74
    setUp: function() {
 
75
        this.input = make_input();
 
76
        this.autocomp = new Y.lazr.AutoComplete({
 
77
            input: this.input
 
78
        });
 
79
    },
 
80
 
 
81
    tearDown: function() {
 
82
        cleanup_widget(this.autocomp);
 
83
        kill_input(this.input);
 
84
    },
 
85
 
 
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 });
 
91
    },
 
92
 
 
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.");
 
97
            return;
 
98
        }
 
99
 
 
100
        var matches = [];
 
101
        this.autocomp
 
102
            .get('boundingBox')
 
103
            .all('.item')
 
104
            .each(function(item) {
 
105
                matches.push(item.get('text'));
 
106
            });
 
107
        return matches;
 
108
    },
 
109
 
 
110
    test_autocomplete_is_visible_if_results_match: function() {
 
111
        this.autocomp.set('data', ['aaa']);
 
112
        this.autocomp.render();
 
113
 
 
114
        // We want to match the one and only data set element.
 
115
        this.complete_input('aa');
 
116
        Assert.isTrue(
 
117
            this.autocomp.get('visible'),
 
118
            "The widget should be visible if matching input was found.");
 
119
    },
 
120
 
 
121
    test_autocomplete_is_hidden_if_no_query_is_given: function() {
 
122
        this.autocomp.set('data', ['aaa']);
 
123
        this.autocomp.render();
 
124
 
 
125
        // We want to simulate an empty input field, but some action triggers
 
126
        // matching.
 
127
        this.complete_input('');
 
128
        Assert.isFalse(
 
129
            this.autocomp.get('visible'),
 
130
            "The widget should be hidden if the input field is empty.");
 
131
    },
 
132
 
 
133
    test_autocomplete_is_hidden_if_results_do_not_match: function() {
 
134
        this.autocomp.set('data', ['bbb']);
 
135
        this.autocomp.render();
 
136
 
 
137
        if (this.autocomp.get('visible')) {
 
138
            Y.fail("The autocomplete widget should start out hidden.");
 
139
        }
 
140
 
 
141
 
 
142
        // 'aa' shouldn't match any of the data.
 
143
        this.complete_input('aa');
 
144
        Assert.isFalse(
 
145
            this.autocomp.get('visible'),
 
146
            "The widget should be hidden if the query doesn't match any " +
 
147
            "possible completions.");
 
148
    },
 
149
 
 
150
    test_display_should_contain_all_matches: function() {
 
151
        var data = [
 
152
            'aaa',
 
153
            'baa'
 
154
        ];
 
155
 
 
156
        this.autocomp.set('data', data);
 
157
        this.autocomp.render();
 
158
 
 
159
        // Trigger autocompletion, should match all data items.
 
160
        this.complete_input('aa');
 
161
 
 
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.");
 
166
 
 
167
        Y.ArrayAssert.itemsAreEqual(
 
168
            this.get_completions(),
 
169
            data,
 
170
            "Every autocomplete item should be present in the available " +
 
171
            "match keys.");
 
172
    },
 
173
 
 
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();
 
178
 
 
179
        // Trigger autocompletion for the loosest matches
 
180
        this.complete_input('aa');
 
181
        // Complete the narrower set
 
182
        this.complete_input('aaa');
 
183
 
 
184
        var completions = this.get_completions();
 
185
 
 
186
        Y.ArrayAssert.itemsAreEqual(
 
187
            ['aaa'],
 
188
            completions,
 
189
            "'aaa' should be the data item displayed after narrowing the " +
 
190
            "search with the query 'aaa'.");
 
191
    },
 
192
 
 
193
    test_matching_text_in_item_is_marked: function() {
 
194
        this.autocomp.set('data', ['aaa']);
 
195
        this.autocomp.render();
 
196
 
 
197
        // Display the matching input.
 
198
        var query = 'aa';
 
199
        this.complete_input(query);
 
200
 
 
201
        // Grab the matching item
 
202
        var matching_text = this.autocomp
 
203
            .get('boundingBox')
 
204
            .one('.item .matching-text');
 
205
 
 
206
        Assert.isNotNull(matching_text,
 
207
            "Some of the matching item's text should be marked as matching.");
 
208
 
 
209
        Assert.areEqual(
 
210
            query,
 
211
            matching_text.get('text'),
 
212
            "The matching text should be the same as the query text.");
 
213
    },
 
214
 
 
215
    test_escape_key_should_close_completions_list: function() {
 
216
        this.autocomp.set('data', ['aaa']);
 
217
        this.autocomp.render();
 
218
 
 
219
        // Open the completions list
 
220
        this.complete_input('aa');
 
221
 
 
222
        // Hit the escape key to close the list
 
223
        Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
 
224
 
 
225
        Assert.isFalse(
 
226
            this.autocomp.get('visible'),
 
227
            "The list of completions should be closed after pressing the " +
 
228
            "escape key.");
 
229
    }
 
230
}));
 
231
 
 
232
suite.add(new Y.Test.Case({
 
233
 
 
234
    name:'test result text marking method',
 
235
 
 
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);
 
239
 
 
240
        Assert.areEqual(
 
241
            '<span class="matching-text">aa</span>bb',
 
242
            marked_text,
 
243
            "The text at the beginning of the result should have been " +
 
244
            "marked.");
 
245
    },
 
246
 
 
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);
 
250
 
 
251
        Assert.areEqual(
 
252
            'b<span class="matching-text">aa</span>b',
 
253
            marked_text,
 
254
            "The text in the middle of the result should have been " +
 
255
            "marked.");
 
256
    },
 
257
 
 
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);
 
261
 
 
262
        Assert.areEqual(
 
263
            'bb<span class="matching-text">aa</span>',
 
264
            marked_text,
 
265
            "The text at the end of the result should have been " +
 
266
            "marked.");
 
267
    }
 
268
}));
 
269
 
 
270
 
 
271
suite.add(new Y.Test.Case({
 
272
 
 
273
    name:'test query parsing',
 
274
 
 
275
    setUp: function() {
 
276
        this.autocomplete = new Y.lazr.AutoComplete({
 
277
            delimiter: ' '
 
278
        });
 
279
    },
 
280
 
 
281
    test_space_for_delimiter: function() {
 
282
        Assert.areEqual(
 
283
            'b',
 
284
            this.autocomplete.parseQuery('a b').text,
 
285
            "Input should be split around the 'space' character.");
 
286
        Assert.isNull(
 
287
            this.autocomplete.parseQuery(' '),
 
288
            "Space for input and delimiter should not parse.");
 
289
    },
 
290
 
 
291
    test_parsed_query_is_stripped_of_leading_whitespace: function() {
 
292
        this.autocomplete.set('delimiter', ',');
 
293
 
 
294
        Assert.areEqual(
 
295
            'a',
 
296
            this.autocomplete.parseQuery(' a').text,
 
297
            "Leading whitespace at the start of the input string should " +
 
298
            "be stripped.");
 
299
 
 
300
        Assert.areEqual(
 
301
            'b',
 
302
            this.autocomplete.parseQuery('a, b').text,
 
303
            "Leading whitespace between the last separator and the current " +
 
304
            "query should be stripped.");
 
305
    },
 
306
 
 
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";
 
310
        var caret = 6;
 
311
 
 
312
        Assert.areEqual(
 
313
            'bbb',
 
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.");
 
317
    },
 
318
 
 
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";
 
322
        var caret = 2;
 
323
 
 
324
        Assert.areEqual(
 
325
            'aaa',
 
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.");
 
329
    }
 
330
}));
 
331
 
 
332
suite.add(new Y.Test.Case({
 
333
 
 
334
    name:'test results matching algorithm',
 
335
 
 
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'!");
 
340
        }
 
341
        if (typeof b == 'undefined') {
 
342
            Assert.fail("Match set 'b' is of type 'undefined'!");
 
343
        }
 
344
        return (a.text == b.text) && (a.offset == b.offset);
 
345
    },
 
346
 
 
347
    test_no_matches_returns_an_empty_array: function() {
 
348
        var autocomplete = new Y.lazr.AutoComplete({
 
349
            data: ['ccc']
 
350
        });
 
351
 
 
352
        var matches = autocomplete.findMatches('aa');
 
353
        Y.ArrayAssert.isEmpty(matches,
 
354
            "No data should have matched the query 'aa'");
 
355
    },
 
356
 
 
357
    test_match_last_item: function() {
 
358
        var autocomplete = new Y.lazr.AutoComplete({
 
359
            data: [
 
360
                'ccc',
 
361
                'bbb',
 
362
                'aaa'
 
363
            ]
 
364
        });
 
365
 
 
366
        var matches = autocomplete.findMatches('aa');
 
367
 
 
368
        Y.ArrayAssert.itemsAreEquivalent(
 
369
            [{text: 'aaa', offset: 0}],
 
370
            matches,
 
371
            this.matches_are_equal,
 
372
            "One row should have matched the query 'aa'.");
 
373
    },
 
374
 
 
375
    test_match_ordering: function() {
 
376
        // Matches, in reverse order.
 
377
        var autocomplete = new Y.lazr.AutoComplete({
 
378
            data: [
 
379
                'bbaa',
 
380
                'baab',
 
381
                'aabb'
 
382
            ]
 
383
        });
 
384
 
 
385
        var matches = autocomplete.findMatches('aa');
 
386
 
 
387
        Y.ArrayAssert.itemsAreEquivalent(
 
388
            [{text: 'aabb', offset: 0},
 
389
             {text: 'baab', offset: 1},
 
390
             {text: 'bbaa', offset: 2}],
 
391
            matches,
 
392
            this.matches_are_equal,
 
393
            "The match array should have all of it's keys in order.");
 
394
    },
 
395
 
 
396
    test_mixed_case_text_matches: function() {
 
397
        var autocomplete = new Y.lazr.AutoComplete({
 
398
            data: ['aBc']
 
399
        });
 
400
 
 
401
        var matches = autocomplete.findMatches('b');
 
402
 
 
403
        Y.ArrayAssert.itemsAreEquivalent(
 
404
            [{text:'aBc', offset: 1}],
 
405
            matches,
 
406
            this.matches_are_equal,
 
407
            "The match algorithm should be case insensitive.");
 
408
    },
 
409
 
 
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']
 
414
        });
 
415
 
 
416
        var matches = autocomplete.findMatches('b');
 
417
 
 
418
        Y.ArrayAssert.itemsAreEquivalent(
 
419
            [{text: 'aBc', offset: 1},
 
420
             {text: 'abc', offset: 1}],
 
421
            matches,
 
422
            this.matches_are_equal,
 
423
            "Mixed-case matches should arrive in stable order.");
 
424
    }
 
425
}));
 
426
 
 
427
 
 
428
suite.add(new Y.Test.Case({
 
429
 
 
430
    name:'test selecting results',
 
431
 
 
432
    setUp: function() {
 
433
        this.input = make_input();
 
434
        this.autocomp = new Y.lazr.AutoComplete({
 
435
            input: this.input
 
436
        });
 
437
        this.autocomp.render();
 
438
    },
 
439
 
 
440
    tearDown: function() {
 
441
        cleanup_widget(this.autocomp);
 
442
        kill_input(this.input);
 
443
    },
 
444
 
 
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 });
 
450
    },
 
451
 
 
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 });
 
455
    },
 
456
 
 
457
    test_pressing_enter_completes_current_input: function() {
 
458
        this.autocomp.set('data', ['aaaa', 'aabb']);
 
459
 
 
460
        // Open the completion options
 
461
        this.complete_input('aa');
 
462
 
 
463
        // Press 'Enter'
 
464
        Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
 
465
 
 
466
        Assert.areEqual(
 
467
            'aaaa ',
 
468
            this.input.value,
 
469
            "The first completion should have been appended to the input's " +
 
470
            "value after pressing the 'Enter' key.");
 
471
    },
 
472
 
 
473
    test_pressing_tab_completes_current_input: function() {
 
474
        this.autocomp.set('data', ['aaaa', 'aabb']);
 
475
 
 
476
        // Open the completion options
 
477
        this.complete_input('aa');
 
478
 
 
479
        // Press 'Tab'
 
480
        Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
 
481
 
 
482
        Assert.areEqual(
 
483
            'aaaa ',
 
484
            this.input.value,
 
485
            "The first completion should have been appended to the input's " +
 
486
            "value after pressing the 'Enter' key.");
 
487
    },
 
488
 
 
489
    test_clicking_on_first_result_completes_input: function() {
 
490
        this.autocomp.set('data', ['aaaa', 'aabb']);
 
491
        this.complete_input('aa');
 
492
 
 
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');
 
497
 
 
498
        Assert.areEqual(
 
499
            'aaaa ',
 
500
            this.input.value,
 
501
            "The first completion should have been appended to the input's " +
 
502
            "value after clicking it's list node.");
 
503
    },
 
504
 
 
505
    test_selecting_results_hides_completion_list: function() {
 
506
        this.autocomp.set('data', 'aaa');
 
507
        this.complete_input('a');
 
508
        this.press_selection_key();
 
509
 
 
510
        Assert.isFalse(
 
511
            this.autocomp.get('visible'),
 
512
            "The completion list should be hidden after a result is " +
 
513
            "selected.");
 
514
    },
 
515
 
 
516
    test_completed_input_replaces_current_input: function() {
 
517
        this.autocomp.set('data', ['abba']);
 
518
 
 
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
 
521
        // work.
 
522
        this.complete_input('xxx b');
 
523
        this.press_selection_key();
 
524
 
 
525
        Assert.areEqual(
 
526
           'xxx abba ',
 
527
           this.input.value,
 
528
           "The user's current query should have been replaced with the " +
 
529
           "selected value.");
 
530
    },
 
531
 
 
532
    test_completed_input_has_delimiter_appended_to_it: function() {
 
533
        var delimiter = ' ';
 
534
        this.autocomp.set('data', ['aaaa']);
 
535
        this.autocomp.set('delimiter', delimiter);
 
536
 
 
537
        this.complete_input('a');
 
538
        this.press_selection_key();
 
539
 
 
540
        Assert.areEqual(
 
541
            delimiter,
 
542
            this.input.value.charAt(this.input.value.length - 1),
 
543
            "The last character of the input should be the current " +
 
544
            "query delimiter.");
 
545
    },
 
546
 
 
547
    test_down_arrow_selects_second_result_in_list: function() {
 
548
        this.autocomp.set('data', ['first_item', 'second_item']);
 
549
 
 
550
        // Match the first result.  It should be selected by default.
 
551
        this.complete_input('item');
 
552
 
 
553
        // Simulate pressing the down arrow key.
 
554
        Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
 
555
 
 
556
        // Now, select the second result.
 
557
        this.press_selection_key();
 
558
 
 
559
        Assert.areEqual(
 
560
            'second_item ',
 
561
            this.input.value,
 
562
            "Pressing the down-arrow key should select the second option " +
 
563
            "in the completions list.");
 
564
    }
 
565
}));
 
566
 
 
567
Y.lazr.testing.Runner.add(suite);
 
568
Y.lazr.testing.Runner.run();
 
569
 
 
570
});