-
Notifications
You must be signed in to change notification settings - Fork 5
/
Select.spec.ts
438 lines (300 loc) · 16.1 KB
/
Select.spec.ts
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
import { mount } from "@vue/test-utils";
import { describe, expect, it } from "vitest";
import VueSelect from "./Select.vue";
const options = [
{ label: "France", value: "FR" },
{ label: "United Kingdom", value: "GB" },
{ label: "United States", value: "US" },
{ label: "Germany", value: "DE" },
];
async function openMenu(wrapper: ReturnType<typeof mount>, method: "mousedown" | "focus-space" | "single-value" = "mousedown") {
if (method === "mousedown") {
await wrapper.get("input").trigger("mousedown");
}
else if (method === "focus-space") {
await wrapper.get("input").trigger("focus");
await wrapper.get("input").trigger("keydown", { key: "Space" });
}
else if (method === "single-value") {
await wrapper.get(".single-value").trigger("click");
}
}
async function dispatchEvent(wrapper: ReturnType<typeof mount>, event: Event) {
document.dispatchEvent(event);
await wrapper.vm.$nextTick();
};
async function inputSearch(wrapper: ReturnType<typeof mount>, search: string) {
await wrapper.get("input").setValue(search);
}
it("should render the component", () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
expect(wrapper.exists()).toBe(true);
});
describe("input + menu interactions behavior", () => {
it("should display the placeholder in the input when no option is selected", () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
expect(wrapper.find("input").attributes("placeholder"));
});
it("should not open the menu when focusing the input", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await wrapper.get("input").trigger("focus");
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
it("should open the menu when triggering mousedown on the input", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
});
it("should open the menu when focusing the input and pressing space", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper, "focus-space");
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
});
it("should open the menu when clicking on the dropdown button", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await wrapper.get(".dropdown-icon").trigger("click");
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
});
it("should not open the menu when is-disabled and an option is selected", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: options[0].value, options, isDisabled: true } });
await openMenu(wrapper, "single-value");
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
it("should close the menu after focusing and pressing tab", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
await wrapper.get("input").trigger("keydown", { key: "Tab" });
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
it("should close the menu when clicking outside the menu", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
await dispatchEvent(wrapper, new MouseEvent("click"));
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
it("should close the menu when hitting escape", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await wrapper.get("input").trigger("mousedown");
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "Escape" }));
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
it("should close the menu when clicking on the dropdown button", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
await wrapper.get(".dropdown-icon").trigger("click");
expect(wrapper.findAll("div[role='option']").length).toBe(0);
});
});
describe("menu on-open focus option", async () => {
it("should focus the first option when opening the menu", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.get(".focused[role='option']").text()).toBe(options[0].label);
});
it("should focus the first available available option when a disabled option is at the first index", async () => {
const options = [
{ label: "Spain", value: "ES", disabled: true },
{ label: "France", value: "FR" },
];
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.get(".focused[role='option']").text()).toBe("France");
});
});
describe("menu keyboard navigation", () => {
it("should navigate through the options with the arrow keys", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(wrapper.get(".focused[role='option']").text()).toBe(options[1].label);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "ArrowUp" }));
expect(wrapper.get(".focused[role='option']").text()).toBe(options[0].label);
});
it("should navigate through the options with the arrow keys and skip disabled options", async () => {
const options = [
{ label: "France", value: "FR" },
{ label: "Spain", value: "ES", disabled: true },
{ label: "United Kingdom", value: "GB" },
];
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(wrapper.get(".focused[role='option']").text()).toBe(options[2].label);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "ArrowUp" }));
expect(wrapper.get(".focused[role='option']").text()).toBe(options[0].label);
});
});
describe("menu filtering", () => {
it("should filter the options when typing in the input", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await inputSearch(wrapper, "United");
expect(wrapper.findAll("div[role='option']").length).toBe(2);
await inputSearch(wrapper, "United States");
expect(wrapper.findAll("div[role='option']").length).toBe(1);
});
});
describe("single-select option", () => {
it("should select an option when clicking on it", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
});
it("should select an option when focusing and pressing enter", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "Enter" }));
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
});
it("should select an option when pressing space", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
// Triggering space event with KeyboardEvent constructor is a bit tricky. Must be done like this:
const event = new KeyboardEvent("keydown", {});
Object.defineProperty(event, "code", { value: "Space" });
Object.defineProperty(event, "key", { value: " " });
document.dispatchEvent(event);
await wrapper.vm.$nextTick();
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
});
it("should remove the selected option when pressing backspace without typing", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
await wrapper.get("input").trigger("mousedown");
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "Backspace" }));
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value], [undefined]]);
expect(wrapper.find(".single-value").exists()).toBe(false);
});
it("should not remove the selected option when pressing backspace after typing", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
await inputSearch(wrapper, "F");
await wrapper.get("input").trigger("keydown", { key: "Backspace" });
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value]]);
expect(wrapper.get(".single-value").element.textContent).toBe(options[0].label);
});
it("cannot select an option when there are no matching options", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await inputSearch(wrapper, "Foo");
expect(wrapper.findAll("div[role='option']").length).toBe(0);
await dispatchEvent(wrapper, new KeyboardEvent("keydown", { key: "Enter" }));
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
});
it("cannot select a disabled option", async () => {
const options = [{ label: "Spain", value: "ES", disabled: true }];
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
});
it("should autofocus the first option when opening the menu, by default", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await openMenu(wrapper);
expect(wrapper.get(".focused[role='option']").text()).toBe(options[0].label);
});
});
describe("multi-select options", () => {
it("should select an option when clicking on it", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.props("modelValue")).toStrictEqual([options[0].value]);
expect(wrapper.get(".multi-value").element.textContent).toBe(options[0].label);
});
it("should display non-selected remaining options on the list", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
await openMenu(wrapper);
expect(wrapper.findAll(".menu-option").length).toBe(options.length - 1);
});
it("should remove a selected option and be able to select it again", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
await openMenu(wrapper);
expect(wrapper.findAll(".menu-option").length).toBe(options.length - 1);
await wrapper.get(".multi-value").trigger("click");
await openMenu(wrapper);
expect(wrapper.findAll(".menu-option").length).toBe(options.length);
expect(wrapper.findAll(".multi-value").length).toBe(0);
});
it("should autofocus the first option when opening the menu, by default", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options } });
await openMenu(wrapper);
expect(wrapper.get(".focused[role='option']").text()).toBe(options[0].label);
});
});
describe("clear button", () => {
it("should display the clear button when an option is selected", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isClearable: true } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
expect(wrapper.find(".clear-button").exists()).toBe(true);
});
it("should clear the selected option when clicking on the clear button", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isClearable: true } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
await wrapper.get(".clear-button").trigger("click");
expect(wrapper.emitted("update:modelValue")).toStrictEqual([[options[0].value], [undefined]]);
expect(wrapper.find(".clear-button").exists()).toBe(false);
});
it("should clear all selected options when clicking on the clear button with isMulti prop", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: [], isMulti: true, options, isClearable: true } });
await openMenu(wrapper);
await wrapper.get("div[role='option']").trigger("click");
await wrapper.get(".clear-button").trigger("click");
expect(wrapper.props("modelValue")).toStrictEqual([options[0].value]);
expect(wrapper.find(".clear-button").exists()).toBe(false);
});
});
describe("search emit", () => {
it("should emit the search event when typing in the input", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await inputSearch(wrapper, "United");
expect(wrapper.emitted("search")).toStrictEqual([["United"]]);
});
it("should emit an empty string for the search when the menu is closed", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options } });
await inputSearch(wrapper, "United");
await dispatchEvent(wrapper, new MouseEvent("click"));
expect(wrapper.emitted("search")).toStrictEqual([["United"], [""]]);
});
});
describe("component props", () => {
it("should display the placeholder in the input when no option is selected", () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, placeholder: "Pick an option" } });
expect(wrapper.find("input").attributes("placeholder")).toBe("Pick an option");
});
it("should disable the input when passing the isDisabled prop", () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isDisabled: true } });
expect(wrapper.get("input").attributes("disabled")).toBe("");
});
it("should not filter menu options when isSearchable prop is set to false", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, isSearchable: false } });
await openMenu(wrapper);
await inputSearch(wrapper, "United");
expect(wrapper.findAll("div[role='option']").length).toBe(options.length);
});
it("should not autofocus an option when passing the autofocus prop", async () => {
const wrapper = mount(VueSelect, { props: { modelValue: null, options, shouldAutofocusOption: false } });
await openMenu(wrapper);
expect(wrapper.findAll(".focused[role='option']")).toHaveLength(0);
});
});