summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/test/browser_rules_class_panel_autocomplete.js
blob: bc0d0a1c2e61f37e2301102e8f0efd4019feeb47 (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
/* Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Test that the autocomplete for the class panel input behaves as expected. The test also
// checks that we're using the cache to retrieve the data when we can do so, and that the
// cache gets cleared, and we're getting data from the server, when there's mutation on
// the page.

const TEST_URI = `${URL_ROOT}doc_class_panel_autocomplete.html`;

add_task(async function () {
  await addTab(TEST_URI);
  const { inspector, view } = await openRuleView();
  const { addEl: textInput } = view.classListPreviewer;
  await selectNode("#auto-div-id-3", inspector);

  info("Open the class panel");
  view.showClassPanel();

  textInput.focus();

  info("Type a letter and check that the popup has the expected items");
  const allClasses = [
    "auto-body-class-1",
    "auto-body-class-2",
    "auto-bold",
    "auto-cssom-primary-color",
    "auto-div-class-1",
    "auto-div-class-2",
    "auto-html-class-1",
    "auto-html-class-2",
    "auto-inline-class-1",
    "auto-inline-class-2",
    "auto-inline-class-3",
    "auto-inline-class-4",
    "auto-inline-class-5",
    "auto-inline-nested-class-1",
    "auto-inline-nested-class-2",
    "auto-inline-nested-class-3",
    "auto-inline-nested-class-4",
    "auto-inline-nested-class-5",
    "auto-inline-nested-class-6",
    "auto-stylesheet-class-1",
    "auto-stylesheet-class-2",
    "auto-stylesheet-class-3",
    "auto-stylesheet-class-4",
    "auto-stylesheet-class-5",
    "auto-stylesheet-nested-class-1",
    "auto-stylesheet-nested-class-2",
    "auto-stylesheet-nested-class-3",
    "auto-stylesheet-nested-class-4",
    "auto-stylesheet-nested-class-5",
    "auto-stylesheet-nested-class-6",
  ];

  const { autocompletePopup } = view.classListPreviewer;
  let onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey("a", {}, view.styleWindow);
  await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");
  await onPopupOpened;
  await checkAutocompleteItems(
    autocompletePopup,
    allClasses,
    "The autocomplete popup has all the classes used in the DOM and in stylesheets"
  );

  info(
    "Test that typing more letters filters the autocomplete popup and uses the cache mechanism"
  );
  EventUtils.sendString("uto-b", view.styleWindow);
  await waitForClassApplied("auto-body-class-1", "#auto-div-id-3");

  await checkAutocompleteItems(
    autocompletePopup,
    allClasses.filter(cls => cls.startsWith("auto-b")),
    "The autocomplete popup was filtered with the content of the input"
  );
  ok(true, "The results were retrieved from the cache mechanism");

  info("Test that autocomplete shows up-to-date results");
  // Modify the content page and assert that the new class is displayed in the
  // autocomplete if the user types a new letter.
  const onNewMutation = inspector.inspectorFront.walker.once("new-mutations");
  await ContentTask.spawn(gBrowser.selectedBrowser, null, async function () {
    content.document.body.classList.add("auto-body-added-by-script");
  });
  await onNewMutation;
  await waitForClassApplied("auto-body-added-by-script", "body");

  // close & reopen the autocomplete so it picks up the added to another element while autocomplete was opened
  let onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
  await onPopupClosed;

  // input is now auto-body
  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.sendString("ody", view.styleWindow);
  await onPopupOpened;
  await checkAutocompleteItems(
    autocompletePopup,
    [
      ...allClasses.filter(cls => cls.startsWith("auto-body")),
      "auto-body-added-by-script",
    ].sort(),
    "The autocomplete popup was filtered with the content of the input"
  );

  info(
    "Test that typing a letter that won't match any of the item closes the popup"
  );
  // input is now auto-bodyy
  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("y", {}, view.styleWindow);
  await waitForClassApplied("auto-bodyy", "#auto-div-id-3");
  await onPopupClosed;
  ok(true, "The popup was closed as expected");
  await checkAutocompleteItems(autocompletePopup, [], "The popup was cleared");

  info("Clear the input and try to autocomplete again");
  textInput.select();
  EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
  // Wait a bit so the debounced function can be executed
  await wait(200);

  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey("a", {}, view.styleWindow);
  await onPopupOpened;

  await checkAutocompleteItems(
    autocompletePopup,
    [...allClasses, "auto-body-added-by-script"].sort(),
    "The autocomplete popup was updated with the new class added to the DOM"
  );

  info("Test keyboard shortcut when the popup is displayed");
  // Escape to hide
  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Escape", {}, view.styleWindow);
  await onPopupClosed;
  ok(true, "The popup was closed when hitting escape");

  // Ctrl + space to show again
  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
  await onPopupOpened;
  ok(true, "Popup was opened again with Ctrl+Space");
  await checkAutocompleteItems(
    autocompletePopup,
    [...allClasses, "auto-body-added-by-script"].sort()
  );

  // Arrow left to hide
  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_ArrowLeft", {}, view.styleWindow);
  await onPopupClosed;
  ok(true, "The popup was closed as when hitting ArrowLeft");

  // Arrow right and Ctrl + space to show again, and Arrow Right to accept
  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
  EventUtils.synthesizeKey(" ", { ctrlKey: true }, view.styleWindow);
  await onPopupOpened;

  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_ArrowRight", {}, view.styleWindow);
  await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
  await onPopupClosed;
  is(
    textInput.value,
    "auto-body-added-by-script",
    "ArrowRight puts the selected item in the input and closes the popup"
  );

  // Backspace to show the list again
  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
  await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
  await onPopupOpened;
  is(
    textInput.value,
    "auto-body-added-by-scrip",
    "ArrowRight puts the selected item in the input and closes the popup"
  );
  await checkAutocompleteItems(
    autocompletePopup,
    ["auto-body-added-by-script"],
    "The autocomplete does show the matching items after hitting backspace"
  );

  // Enter to accept
  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Enter", {}, view.styleWindow);
  await waitForClassRemoved("auto-body-added-by-scrip");
  await onPopupClosed;
  is(
    textInput.value,
    "auto-body-added-by-script",
    "Enter puts the selected item in the input and closes the popup"
  );

  // Backspace to show again
  onPopupOpened = autocompletePopup.once("popup-opened");
  EventUtils.synthesizeKey("KEY_Backspace", {}, view.styleWindow);
  await waitForClassApplied("auto-body-added-by-script", "#auto-div-id-3");
  await onPopupOpened;
  is(
    textInput.value,
    "auto-body-added-by-scrip",
    "ArrowRight puts the selected item in the input and closes the popup"
  );
  await checkAutocompleteItems(
    autocompletePopup,
    ["auto-body-added-by-script"],
    "The autocomplete does show the matching items after hitting backspace"
  );

  // Tab to accept
  onPopupClosed = autocompletePopup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Tab", {}, view.styleWindow);
  await onPopupClosed;
  is(
    textInput.value,
    "auto-body-added-by-script",
    "Tab puts the selected item in the input and closes the popup"
  );
  await waitForClassRemoved("auto-body-added-by-scrip");
});

async function checkAutocompleteItems(
  autocompletePopup,
  expectedItems,
  assertionMessage
) {
  await waitForSuccess(
    () =>
      getAutocompleteItems(autocompletePopup).length === expectedItems.length
  );
  const items = getAutocompleteItems(autocompletePopup);
  Assert.deepEqual(items, expectedItems, assertionMessage);
}

function getAutocompleteItems(autocompletePopup) {
  return Array.from(autocompletePopup._panel.querySelectorAll("li")).map(
    el => el.textContent
  );
}

async function waitForClassApplied(cls, selector) {
  info("Wait for class to be applied: " + cls);
  await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [cls, selector],
    async (_cls, _selector) => {
      return ContentTaskUtils.waitForCondition(() =>
        content.document.querySelector(_selector).classList.contains(_cls)
      );
    }
  );
  // Wait for debounced functions to be executed
  await wait(200);
}

async function waitForClassRemoved(cls) {
  info("Wait for class to be removed: " + cls);
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [cls], async _cls => {
    return ContentTaskUtils.waitForCondition(
      () =>
        !content.document
          .querySelector("#auto-div-id-3")
          .classList.contains(_cls)
    );
  });
  // Wait for debounced functions to be executed
  await wait(200);
}