@instructure/ui-tree-browser 10.16.1 → 10.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/es/TreeBrowser/TreeButton/__new-tests__/TreeButton.test.js +165 -0
  3. package/es/TreeBrowser/TreeCollection/__new-tests__/TreeCollection.test.js +454 -0
  4. package/es/TreeBrowser/{TreeBrowserLocator.js → TreeNode/__new-tests__/TreeNode.test.js} +31 -14
  5. package/es/TreeBrowser/__new-tests__/TreeBrowser.test.js +525 -0
  6. package/lib/TreeBrowser/TreeButton/__new-tests__/TreeButton.test.js +166 -0
  7. package/lib/TreeBrowser/TreeCollection/__new-tests__/TreeCollection.test.js +457 -0
  8. package/lib/TreeBrowser/TreeNode/__new-tests__/TreeNode.test.js +56 -0
  9. package/lib/TreeBrowser/__new-tests__/TreeBrowser.test.js +527 -0
  10. package/package.json +17 -14
  11. package/src/TreeBrowser/TreeButton/__new-tests__/TreeButton.test.tsx +162 -0
  12. package/src/TreeBrowser/TreeCollection/__new-tests__/TreeCollection.test.tsx +423 -0
  13. package/src/TreeBrowser/{TreeBrowserLocator.ts → TreeNode/__new-tests__/TreeNode.test.tsx} +30 -13
  14. package/src/TreeBrowser/__new-tests__/TreeBrowser.test.tsx +575 -0
  15. package/tsconfig.build.json +1 -1
  16. package/tsconfig.build.tsbuildinfo +1 -1
  17. package/types/TreeBrowser/TreeButton/__new-tests__/TreeButton.test.d.ts +2 -0
  18. package/types/TreeBrowser/TreeButton/__new-tests__/TreeButton.test.d.ts.map +1 -0
  19. package/types/TreeBrowser/TreeCollection/__new-tests__/TreeCollection.test.d.ts +2 -0
  20. package/types/TreeBrowser/TreeCollection/__new-tests__/TreeCollection.test.d.ts.map +1 -0
  21. package/types/TreeBrowser/TreeNode/__new-tests__/TreeNode.test.d.ts +2 -0
  22. package/types/TreeBrowser/TreeNode/__new-tests__/TreeNode.test.d.ts.map +1 -0
  23. package/types/TreeBrowser/__new-tests__/TreeBrowser.test.d.ts +2 -0
  24. package/types/TreeBrowser/__new-tests__/TreeBrowser.test.d.ts.map +1 -0
  25. package/es/TreeBrowser/locator.js +0 -26
  26. package/lib/TreeBrowser/TreeBrowserLocator.js +0 -44
  27. package/lib/TreeBrowser/locator.js +0 -37
  28. package/src/TreeBrowser/locator.ts +0 -27
  29. package/types/TreeBrowser/TreeBrowserLocator.d.ts +0 -1065
  30. package/types/TreeBrowser/TreeBrowserLocator.d.ts.map +0 -1
  31. package/types/TreeBrowser/locator.d.ts +0 -4
  32. package/types/TreeBrowser/locator.d.ts.map +0 -1
@@ -0,0 +1,575 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { render, screen, waitFor } from '@testing-library/react'
26
+ import userEvent from '@testing-library/user-event'
27
+ import { vi } from 'vitest'
28
+ import type { MockInstance } from 'vitest'
29
+ import { runAxeCheck } from '@instructure/ui-axe-check'
30
+
31
+ import '@testing-library/jest-dom'
32
+ import { TreeBrowser } from '../index'
33
+ import { TreeNode } from '../TreeNode'
34
+
35
+ const COLLECTIONS_DATA = {
36
+ 2: { id: 2, name: 'Root Directory', collections: [3, 4], items: [1] },
37
+ 3: { id: 3, name: 'Sub Root 1', collections: [5] },
38
+ 4: { id: 4, name: 'Sub Root 2' },
39
+ 5: { id: 5, name: 'Nested Sub Collection' }
40
+ }
41
+
42
+ const COLLECTIONS_DATA_WITH_ZERO = {
43
+ 0: { id: 0, name: 'Root Directory', collections: [3, 4], items: [1] },
44
+ 3: { id: 3, name: 'Sub Root 1', collections: [5] },
45
+ 4: { id: 4, name: 'Sub Root 2' },
46
+ 5: { id: 5, name: 'Nested Sub Collection' }
47
+ }
48
+
49
+ const COLLECTIONS_DATA_WITH_STRING_IDS = {
50
+ '2': { id: '2', name: 'Root Directory', collections: ['3', '4'], items: [1] },
51
+ '3': { id: '3', name: 'Sub Root 1', collections: ['5'] },
52
+ '4': { id: '4', name: 'Sub Root 2' },
53
+ '5': { id: '5', name: 'Nested Sub Collection' }
54
+ }
55
+
56
+ const ITEMS_DATA = {
57
+ 1: { id: 1, name: 'Item 1' }
58
+ }
59
+
60
+ describe('<TreeBrowser />', () => {
61
+ let consoleWarningMock: ReturnType<typeof vi.spyOn>
62
+ let consoleErrorMock: ReturnType<typeof vi.spyOn>
63
+
64
+ beforeEach(() => {
65
+ // Mocking console to prevent test output pollution and expect for messages
66
+ consoleWarningMock = vi
67
+ .spyOn(console, 'warn')
68
+ .mockImplementation(() => {}) as MockInstance
69
+
70
+ consoleErrorMock = vi
71
+ .spyOn(console, 'error')
72
+ .mockImplementation(() => {}) as MockInstance
73
+ })
74
+
75
+ afterEach(() => {
76
+ consoleWarningMock.mockRestore()
77
+ consoleErrorMock.mockRestore()
78
+ })
79
+
80
+ it('should render a tree', async () => {
81
+ const { container } = render(
82
+ <TreeBrowser
83
+ collections={COLLECTIONS_DATA}
84
+ items={ITEMS_DATA}
85
+ rootId={2}
86
+ />
87
+ )
88
+ const tree = container.querySelector('[class$="-treeBrowser"]')
89
+
90
+ expect(tree).toBeInTheDocument()
91
+ })
92
+
93
+ it('should render subcollections', async () => {
94
+ render(
95
+ <TreeBrowser
96
+ collections={COLLECTIONS_DATA}
97
+ items={ITEMS_DATA}
98
+ rootId={2}
99
+ />
100
+ )
101
+ const items = screen.getAllByRole('treeitem')
102
+
103
+ expect(items.length).toEqual(1)
104
+
105
+ await userEvent.click(items[0])
106
+
107
+ await waitFor(() => {
108
+ const itemsAfterClick = screen.getAllByRole('treeitem')
109
+ expect(itemsAfterClick.length).toEqual(4)
110
+ })
111
+ })
112
+
113
+ it('should render all collections at top level if showRootCollection is true and rootId is undefined', async () => {
114
+ render(
115
+ <TreeBrowser
116
+ collections={COLLECTIONS_DATA}
117
+ items={ITEMS_DATA}
118
+ rootId={undefined}
119
+ />
120
+ )
121
+ const items = screen.getAllByRole('treeitem')
122
+
123
+ expect(items.length).toEqual(4)
124
+ })
125
+
126
+ describe('expanded', () => {
127
+ it('should not expand collections or items without defaultExpanded prop', async () => {
128
+ render(
129
+ <TreeBrowser
130
+ collections={COLLECTIONS_DATA}
131
+ items={ITEMS_DATA}
132
+ rootId={2}
133
+ />
134
+ )
135
+ const items = screen.getAllByRole('treeitem')
136
+
137
+ expect(items.length).toEqual(1)
138
+ expect(items[0]).toHaveTextContent('Root Directory')
139
+ })
140
+
141
+ it('should accept an array of default expanded collections', async () => {
142
+ render(
143
+ <TreeBrowser
144
+ collections={COLLECTIONS_DATA}
145
+ items={ITEMS_DATA}
146
+ rootId={2}
147
+ defaultExpanded={[2, 3]}
148
+ />
149
+ )
150
+ const items = screen.getAllByRole('treeitem')
151
+ const subRoot2 = screen.getByLabelText('Sub Root 2')
152
+ const nestedSub = screen.getByLabelText('Nested Sub Collection')
153
+
154
+ expect(items.length).toEqual(5)
155
+
156
+ expect(subRoot2).toHaveAttribute('aria-label', 'Sub Root 2')
157
+ expect(subRoot2).toHaveTextContent('Sub Root 2')
158
+
159
+ expect(nestedSub).toHaveAttribute('aria-label', 'Nested Sub Collection')
160
+ expect(nestedSub).toHaveTextContent('Nested Sub Collection')
161
+ })
162
+ })
163
+
164
+ describe('selected', () => {
165
+ it('should not show the selection if selectionType is none', async () => {
166
+ render(
167
+ <TreeBrowser
168
+ collections={COLLECTIONS_DATA}
169
+ items={ITEMS_DATA}
170
+ rootId={2}
171
+ />
172
+ )
173
+ const item = screen.getByRole('treeitem')
174
+
175
+ await userEvent.click(item)
176
+
177
+ await waitFor(() => {
178
+ expect(item).not.toHaveAttribute('aria-selected')
179
+ })
180
+ })
181
+
182
+ it('should show the selection indicator on last clicked collection or item', async () => {
183
+ render(
184
+ <TreeBrowser
185
+ collections={COLLECTIONS_DATA}
186
+ items={ITEMS_DATA}
187
+ rootId={2}
188
+ selectionType="single"
189
+ />
190
+ )
191
+ const item = screen.getByLabelText('Root Directory')
192
+
193
+ await userEvent.click(item)
194
+
195
+ await waitFor(() => {
196
+ expect(item).toHaveAttribute('aria-selected')
197
+ })
198
+
199
+ const nestedItem = screen.getByLabelText('Item 1')
200
+
201
+ await userEvent.click(nestedItem)
202
+
203
+ await waitFor(() => {
204
+ expect(nestedItem).toHaveAttribute('aria-selected')
205
+ })
206
+ })
207
+ })
208
+
209
+ describe('collections', () => {
210
+ it('should render collections with string-keyed ids', async () => {
211
+ render(
212
+ <TreeBrowser
213
+ collections={COLLECTIONS_DATA_WITH_STRING_IDS}
214
+ items={ITEMS_DATA}
215
+ rootId={'2'}
216
+ showRootCollection={true}
217
+ />
218
+ )
219
+ const item = screen.getByLabelText('Root Directory')
220
+
221
+ expect(item).toBeInTheDocument()
222
+ })
223
+
224
+ it('should not show the first keyed collection if showRootCollection is false', async () => {
225
+ render(
226
+ <TreeBrowser
227
+ collections={COLLECTIONS_DATA}
228
+ items={ITEMS_DATA}
229
+ rootId={2}
230
+ showRootCollection={false}
231
+ />
232
+ )
233
+ const items = screen.getAllByRole('treeitem')
234
+
235
+ expect(items.length).toEqual(3)
236
+ })
237
+
238
+ it('should render first keyed collection if showRootCollection is true and rootId specified', async () => {
239
+ render(
240
+ <TreeBrowser
241
+ collections={COLLECTIONS_DATA}
242
+ items={ITEMS_DATA}
243
+ rootId={2}
244
+ />
245
+ )
246
+ const item = screen.getByLabelText('Root Directory')
247
+
248
+ expect(item).toBeInTheDocument()
249
+ })
250
+
251
+ it('should not show the first keyed collection if showRootCollection is false and rootId is 0', async () => {
252
+ render(
253
+ <TreeBrowser
254
+ collections={COLLECTIONS_DATA_WITH_ZERO}
255
+ items={ITEMS_DATA}
256
+ rootId={0}
257
+ showRootCollection={false}
258
+ />
259
+ )
260
+ const items = screen.getAllByRole('treeitem')
261
+
262
+ expect(items.length).toEqual(3)
263
+ })
264
+
265
+ it('should render a folder icon by default', async () => {
266
+ const { container } = render(
267
+ <TreeBrowser
268
+ collections={COLLECTIONS_DATA}
269
+ items={ITEMS_DATA}
270
+ rootId={2}
271
+ />
272
+ )
273
+ const iconFolder = container.querySelectorAll('svg[name="IconFolder"]')
274
+
275
+ expect(iconFolder.length).toEqual(1)
276
+ })
277
+
278
+ it('should render a custom icon', async () => {
279
+ const IconCustom = (
280
+ <svg height="100" width="100" data-testid="icon-custom">
281
+ <title data-testid="icon-custom-title">Custom icon</title>
282
+ <circle cx="50" cy="50" r="40" />
283
+ </svg>
284
+ )
285
+
286
+ render(
287
+ <TreeBrowser
288
+ collections={COLLECTIONS_DATA}
289
+ items={ITEMS_DATA}
290
+ rootId={2}
291
+ collectionIcon={() => IconCustom}
292
+ />
293
+ )
294
+ const iconCustom = screen.getByTestId('icon-custom')
295
+ const title = screen.getByTestId('icon-custom-title')
296
+
297
+ expect(iconCustom).toBeInTheDocument()
298
+ expect(title).toBeInTheDocument()
299
+ expect(title).toHaveTextContent('Custom icon')
300
+ })
301
+
302
+ it('should render without icon if set to null', async () => {
303
+ const { container } = render(
304
+ <TreeBrowser
305
+ collections={COLLECTIONS_DATA}
306
+ items={ITEMS_DATA}
307
+ rootId={2}
308
+ collectionIcon={null}
309
+ />
310
+ )
311
+ const icon = container.querySelector('svg')
312
+
313
+ expect(icon).not.toBeInTheDocument()
314
+ })
315
+
316
+ it('should call onCollectionToggle when expanding and collapsing with mouse', async () => {
317
+ const onCollectionToggle = vi.fn()
318
+
319
+ render(
320
+ <TreeBrowser
321
+ collections={COLLECTIONS_DATA}
322
+ items={ITEMS_DATA}
323
+ rootId={2}
324
+ onCollectionToggle={onCollectionToggle}
325
+ />
326
+ )
327
+ const item = screen.getByRole('treeitem')
328
+
329
+ await userEvent.click(item)
330
+
331
+ await waitFor(() => {
332
+ expect(onCollectionToggle).toHaveBeenCalled()
333
+ })
334
+ })
335
+
336
+ it('should call onCollectionClick on button activation (space/enter or click)', async () => {
337
+ const onCollectionClick = vi.fn()
338
+
339
+ render(
340
+ <TreeBrowser
341
+ collections={COLLECTIONS_DATA}
342
+ items={ITEMS_DATA}
343
+ rootId={2}
344
+ onCollectionClick={onCollectionClick}
345
+ />
346
+ )
347
+ const item = screen.getByLabelText('Root Directory')
348
+
349
+ await userEvent.click(item)
350
+ await userEvent.type(item, '{space}')
351
+ await userEvent.type(item, '{enter}')
352
+
353
+ await waitFor(() => {
354
+ expect(onCollectionClick).toHaveBeenCalledTimes(3)
355
+ })
356
+ })
357
+
358
+ it('should render before, after nodes of the provided collection', async () => {
359
+ const { container } = render(
360
+ <TreeBrowser
361
+ collections={{
362
+ 2: {
363
+ id: 2,
364
+ name: 'Root Directory',
365
+ collections: [],
366
+ items: [],
367
+ renderBeforeItems: (
368
+ <TreeNode>
369
+ <input id="input-before" />
370
+ </TreeNode>
371
+ ),
372
+ renderAfterItems: (
373
+ <TreeNode>
374
+ <input id="input-after" />
375
+ </TreeNode>
376
+ )
377
+ }
378
+ }}
379
+ items={{}}
380
+ expanded={[2]}
381
+ rootId={2}
382
+ />
383
+ )
384
+ const contentBefore = container.querySelector('#input-before')
385
+ const contentAfter = container.querySelector('#input-after')
386
+
387
+ expect(contentBefore).toBeInTheDocument()
388
+ expect(contentAfter).toBeInTheDocument()
389
+ })
390
+ })
391
+
392
+ describe('items', () => {
393
+ it('should render a document icon by default', async () => {
394
+ const { container } = render(
395
+ <TreeBrowser
396
+ collections={COLLECTIONS_DATA}
397
+ items={ITEMS_DATA}
398
+ rootId={2}
399
+ defaultExpanded={[2]}
400
+ />
401
+ )
402
+ const iconDocument = container.querySelectorAll(
403
+ 'svg[name="IconDocument"]'
404
+ )
405
+
406
+ expect(iconDocument.length).toEqual(1)
407
+ })
408
+
409
+ it('should render a custom icon', async () => {
410
+ const IconCustom = (
411
+ <svg height="100" width="100" data-testid="icon-custom">
412
+ <title data-testid="icon-custom-title">Custom icon</title>
413
+ <circle cx="50" cy="50" r="40" />
414
+ </svg>
415
+ )
416
+
417
+ render(
418
+ <TreeBrowser
419
+ collections={COLLECTIONS_DATA}
420
+ items={ITEMS_DATA}
421
+ rootId={2}
422
+ defaultExpanded={[2]}
423
+ itemIcon={() => IconCustom}
424
+ />
425
+ )
426
+ const iconCustom = screen.getByTestId('icon-custom')
427
+ const title = screen.getByTestId('icon-custom-title')
428
+
429
+ expect(iconCustom).toBeInTheDocument()
430
+ expect(title).toBeInTheDocument()
431
+ expect(title).toHaveTextContent('Custom icon')
432
+ })
433
+
434
+ it('should render without icon if set to null', async () => {
435
+ const { container } = render(
436
+ <TreeBrowser
437
+ collections={COLLECTIONS_DATA}
438
+ items={ITEMS_DATA}
439
+ rootId={2}
440
+ />
441
+ )
442
+ const iconDocument = container.querySelector('svg[name="IconDocument"]')
443
+
444
+ expect(iconDocument).not.toBeInTheDocument()
445
+ })
446
+ })
447
+
448
+ describe('for a11y', () => {
449
+ it('should meet a11y standards', async () => {
450
+ const { container } = render(
451
+ <TreeBrowser
452
+ collections={COLLECTIONS_DATA}
453
+ items={ITEMS_DATA}
454
+ rootId={2}
455
+ />
456
+ )
457
+ const axeCheck = await runAxeCheck(container)
458
+ expect(axeCheck).toBe(true)
459
+ })
460
+
461
+ it('should accept a treeLabel prop', async () => {
462
+ render(
463
+ <TreeBrowser
464
+ collections={COLLECTIONS_DATA}
465
+ items={ITEMS_DATA}
466
+ rootId={2}
467
+ treeLabel="Test treeLabel"
468
+ />
469
+ )
470
+ const tree = screen.getByLabelText('Test treeLabel')
471
+ expect(tree).toBeInTheDocument()
472
+ })
473
+
474
+ it('should toggle aria-expanded', async () => {
475
+ render(
476
+ <TreeBrowser
477
+ collections={COLLECTIONS_DATA}
478
+ items={ITEMS_DATA}
479
+ rootId={2}
480
+ />
481
+ )
482
+ const item = screen.getByRole('treeitem')
483
+
484
+ expect(item).toHaveAttribute('aria-expanded', 'false')
485
+
486
+ await userEvent.click(item)
487
+
488
+ await waitFor(() => {
489
+ expect(item).toHaveAttribute('aria-expanded', 'true')
490
+ })
491
+ })
492
+
493
+ it('should use aria-selected when selectionType is not none', async () => {
494
+ render(
495
+ <TreeBrowser
496
+ collections={COLLECTIONS_DATA}
497
+ items={ITEMS_DATA}
498
+ rootId={2}
499
+ selectionType="single"
500
+ />
501
+ )
502
+ const item = screen.getByRole('treeitem')
503
+ expect(item).not.toHaveAttribute('aria-selected')
504
+
505
+ await userEvent.click(item)
506
+
507
+ await waitFor(() => {
508
+ expect(item).toHaveAttribute('aria-selected', 'true')
509
+ })
510
+
511
+ const nestedItem = screen.getByLabelText('Sub Root 1')
512
+ expect(nestedItem).toHaveAttribute('aria-selected', 'false')
513
+ })
514
+ })
515
+
516
+ describe('sorting', () => {
517
+ it("should present collections and items in alphabetical order, in spite of the order of 'collections' and 'items' arrays", async () => {
518
+ render(
519
+ <TreeBrowser
520
+ collections={{
521
+ 1: {
522
+ id: 1,
523
+ name: 'Assignments',
524
+ collections: [5, 3, 2, 4],
525
+ items: [3, 5, 2, 1, 4]
526
+ },
527
+ 2: {
528
+ id: 2,
529
+ name: 'English Assignments',
530
+ collections: [],
531
+ items: []
532
+ },
533
+ 3: { id: 3, name: 'Math Assignments', collections: [], items: [] },
534
+ 4: {
535
+ id: 4,
536
+ name: 'Reading Assignments',
537
+ collections: [],
538
+ items: []
539
+ },
540
+ 5: { id: 5, name: 'Advanced Math Assignments', items: [] }
541
+ }}
542
+ items={{
543
+ 1: { id: 1, name: 'Addition Worksheet' },
544
+ 2: { id: 2, name: 'Subtraction Worksheet' },
545
+ 3: { id: 3, name: 'General Questions' },
546
+ 4: { id: 4, name: 'Vogon Poetry' },
547
+ 5: { id: 5, name: 'Bistromath' }
548
+ }}
549
+ rootId={1}
550
+ defaultExpanded={[1]}
551
+ sortOrder={(a, b) => {
552
+ return a.name.localeCompare(b.name)
553
+ }}
554
+ />
555
+ )
556
+ const items = screen.getAllByRole('treeitem')
557
+
558
+ const arr = items.map((item) => item.textContent)
559
+ expect(arr.slice(1, 5)).toStrictEqual([
560
+ 'Advanced Math Assignments',
561
+ 'English Assignments',
562
+ 'Math Assignments',
563
+ 'Reading Assignments'
564
+ ])
565
+
566
+ expect(arr.slice(5)).toStrictEqual([
567
+ 'Addition Worksheet',
568
+ 'Bistromath',
569
+ 'General Questions',
570
+ 'Subtraction Worksheet',
571
+ 'Vogon Poetry'
572
+ ])
573
+ })
574
+ })
575
+ })
@@ -7,9 +7,9 @@
7
7
  },
8
8
  "include": ["src"],
9
9
  "references": [
10
+ { "path": "../ui-axe-check/tsconfig.build.json" },
10
11
  { "path": "../ui-babel-preset/tsconfig.build.json" },
11
12
  { "path": "../ui-color-utils/tsconfig.build.json" },
12
- { "path": "../ui-test-locator/tsconfig.build.json" },
13
13
  { "path": "../ui-test-utils/tsconfig.build.json" },
14
14
  { "path": "../ui-themes/tsconfig.build.json" },
15
15
  { "path": "../emotion/tsconfig.build.json" },