summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html
blob: 110d5640c1e74f0652cbeb0ef83421d1689a0651 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html>
<html>
<!--
Test the searchbox and autocomplete-popup components
-->
<head>
  <meta charset="utf-8">
  <title>SearchBox component test</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script src="head.js"></script>
<script>
"use strict";
window.onload = async function () {
  /**
   * Takes a DOMNode with its children as list items,
   * Typically UL > LI and each item's text value is
   * compared with the reference item's value as a test
   *
   * @params {Node} - Node to be compared
   * @reference {array} - Reference array for comparison. The selected index is
   * highlighted as a single element array ie. ["[abc]", "ab", "abcPQR"],
   * Here the element "abc" is highlighted
   */
  function compareAutocompleteList(list, reference) {
    const delimiter = " - ";
    const observedList = [...list.children].map(el => {
      return el.classList.contains("autocomplete-selected")
        ? `[${el.textContent}]`
        : el.textContent
    });
    is(observedList.join(delimiter), reference.join(delimiter),
      "Autocomplete items are rendered as expected");
  }

  function compareCursorPosition(initialElement) {
    const initialPosition = initialElement.selectionStart;
    return (element) => {
      is(element.selectionStart, initialPosition, "Input cursor position is not changed");
    }
  }

  const React = browserRequire("devtools/client/shared/vendor/react");
  const SearchBox = React.createFactory(
    browserRequire("devtools/client/shared/components/SearchBox")
  );
  const { component, $ } = await createComponentTest(SearchBox, {
    type: "search",
    autocompleteProvider: (filter) => {
      const baseList = [
        "foo",
        "BAR",
        "baZ",
        "abc",
        "pqr",
        "xyz",
        "ABC",
        "a1",
        "a2",
        "a3",
        "a4",
        "a5",
      ];
      if (!filter) {
        return [];
      }

      const tokens = filter.split(/\s+/g);
      const lastToken = tokens[tokens.length - 1];
      const previousTokens = tokens.slice(0, tokens.length - 1);

      if (!lastToken) {
        return [];
      }

      return baseList
        .filter((item) => {
          return item.toLowerCase().startsWith(lastToken.toLowerCase())
            && item.toLowerCase() !== lastToken.toLowerCase();
        })
        .sort()
        .map(item => ({
          value: [...previousTokens, item].join(" "),
          displayValue: item,
        }));
    },
    onChange: () => null,
  });

  async function testSearchBoxWithAutocomplete() {
    ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");

    $(".devtools-searchinput").focus();
    await forceRender(component); // Wait for state update
    ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");

    sendString("a");
    await forceRender(component);

    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "[ABC]",
      "a1",
      "a2",
      "a3",
      "a4",
      "a5",
      "abc",
    ]);

    // Blur event
    $(".devtools-searchinput").blur();
    await forceRender(component);
    ok(!component.state.focused, "focused state was properly set");
    ok(!$(".devtools-autocomplete-popup"), "Autocomplete list removed from DOM");
  }

  async function testKeyEventsWithAutocomplete() {
    // Clear the initial input
    $(".devtools-searchinput").focus();
    const cursorPositionIsNotChanged = compareCursorPosition($(".devtools-searchinput"));

    // ArrowDown
    synthesizeKey("KEY_ArrowDown");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "ABC",
      "[a1]",
      "a2",
      "a3",
      "a4",
      "a5",
      "abc",
    ]);
    ok($(".devtools-autocomplete-listbox .autocomplete-item:nth-child(2)")
      .className.includes("autocomplete-selected"),
      "Selection class applied");

    // A double ArrowUp should roll back to the bottom of the list
    synthesizeKey("KEY_ArrowUp");
    synthesizeKey("KEY_ArrowUp");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "ABC",
      "a1",
      "a2",
      "a3",
      "a4",
      "a5",
      "[abc]",
    ]);
    cursorPositionIsNotChanged($(".devtools-searchinput"));

    // PageDown should take -5 places up
    synthesizeKey("KEY_PageUp");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "ABC",
      "[a1]",
      "a2",
      "a3",
      "a4",
      "a5",
      "abc",
    ]);
    cursorPositionIsNotChanged($(".devtools-searchinput"));

    // PageDown should take +5 places down
    synthesizeKey("KEY_PageDown");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "ABC",
      "a1",
      "a2",
      "a3",
      "a4",
      "a5",
      "[abc]",
    ]);
    cursorPositionIsNotChanged($(".devtools-searchinput"));

    // Home should take to the top of the list
    synthesizeKey("KEY_Home");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "[ABC]",
      "a1",
      "a2",
      "a3",
      "a4",
      "a5",
      "abc",
    ]);
    cursorPositionIsNotChanged($(".devtools-searchinput"));

    // End should take to the bottom of the list
    synthesizeKey("KEY_End");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "ABC",
      "a1",
      "a2",
      "a3",
      "a4",
      "a5",
      "[abc]",
    ]);
    cursorPositionIsNotChanged($(".devtools-searchinput"));

    // Key down in existing state should rollover to the top
    synthesizeKey("KEY_ArrowDown");
    await forceRender(component);
    // Tab should select the component and hide popup
    synthesizeKey("KEY_Tab");
    await forceRender(component);
    is(component.state.value, "ABC", "Tab hit selects the item");
    ok(!$(".devtools-autocomplete-popup"), "Tab hit hides the popup");

    // Activate popup by removing a key
    synthesizeKey("KEY_Backspace");
    await forceRender(component);
    ok($(".devtools-autocomplete-popup"), "Popup is up");
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "[ABC]",
      "abc"
    ]);

    // Enter key selection
    synthesizeKey("KEY_ArrowUp");
    await forceRender(component);
    synthesizeKey("KEY_Enter");
    is(component.state.value, "abc", "Enter selection");
    ok(!$(".devtools-autocomplete-popup"), "Enter/Return hides the popup");

    // Escape should remove the autocomplete component
    synthesizeKey("KEY_Backspace");
    await forceRender(component);
    synthesizeKey("KEY_Escape");
    await forceRender(component);
    ok(!$(".devtools-autocomplete-popup"),
      "Autocomplete list removed from DOM on Escape");
  }

  async function testMouseEventsWithAutocomplete() {
    $(".devtools-searchinput").focus();
    await setState(component, {
      value: "",
      focused: true,
    });
    await forceRender(component);

    // ArrowDown
    synthesizeKey("KEY_ArrowDown");
    await forceRender(component);
    synthesizeMouseAtCenter($(".devtools-searchinput"), {}, window);
    await forceRender(component);
    is(component.state.focused, true, "Component should now be focused");

    sendString("pq");
    await forceRender(component);
    synthesizeMouseAtCenter(
      $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
      {}, window
    );
    await forceRender(component);
    is(component.state.value, "pqr", "Mouse click selects the item.");
    ok(!$(".devtools-autocomplete-popup"), "Mouse click on item hides the popup");
  }

  async function testTokenizedAutocomplete() {
    // Test for string "pqr ab" which should show list of ABC, abc
    sendString(" ab");
    await forceRender(component);
    compareAutocompleteList($(".devtools-autocomplete-listbox"), [
      "[ABC]",
      "abc"
    ]);

    // Select the first element, value now should be "pqr ABC"
    synthesizeMouseAtCenter(
      $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
      {}, window
    );
    is(component.state.value, "pqr ABC", "Post Tokenization value selection");
  }

  add_task(async function () {
    await testSearchBoxWithAutocomplete();
    await testKeyEventsWithAutocomplete();
    await testMouseEventsWithAutocomplete();
    await testTokenizedAutocomplete();
  });
};
</script>
</body>
</html>