@internetarchive/collection-browser 4.1.0 → 4.2.0-alpha-webdev8164.0

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 (62) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +27 -27
  3. package/.github/workflows/gh-pages-main.yml +39 -39
  4. package/.github/workflows/npm-publish.yml +39 -39
  5. package/.github/workflows/pr-preview.yml +38 -38
  6. package/.husky/pre-commit +1 -1
  7. package/.prettierignore +1 -1
  8. package/LICENSE +661 -661
  9. package/README.md +83 -83
  10. package/dist/src/collection-browser.js +761 -761
  11. package/dist/src/collection-browser.js.map +1 -1
  12. package/dist/src/collection-facets/facets-template.js +5 -0
  13. package/dist/src/collection-facets/facets-template.js.map +1 -1
  14. package/dist/src/collection-facets/more-facets-content.d.ts +92 -8
  15. package/dist/src/collection-facets/more-facets-content.js +526 -84
  16. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  17. package/dist/src/collection-facets/more-facets-pagination.d.ts +12 -3
  18. package/dist/src/collection-facets/more-facets-pagination.js +69 -8
  19. package/dist/src/collection-facets/more-facets-pagination.js.map +1 -1
  20. package/dist/src/collection-facets/toggle-switch.js +1 -0
  21. package/dist/src/collection-facets/toggle-switch.js.map +1 -1
  22. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  23. package/dist/src/data-source/collection-browser-query-state.js.map +1 -1
  24. package/dist/src/sort-filter-bar/sort-filter-bar.js +280 -280
  25. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  26. package/dist/test/collection-browser.test.js +189 -189
  27. package/dist/test/collection-browser.test.js.map +1 -1
  28. package/dist/test/collection-facets/more-facets-content.test.js +162 -3
  29. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  30. package/dist/test/collection-facets/more-facets-pagination.test.js +63 -3
  31. package/dist/test/collection-facets/more-facets-pagination.test.js.map +1 -1
  32. package/dist/test/mocks/mock-search-responses.d.ts +5 -0
  33. package/dist/test/mocks/mock-search-responses.js +44 -0
  34. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  35. package/dist/test/mocks/mock-search-service.js +2 -1
  36. package/dist/test/mocks/mock-search-service.js.map +1 -1
  37. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +22 -22
  38. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  39. package/eslint.config.mjs +53 -53
  40. package/index.html +24 -24
  41. package/local.archive.org.cert +86 -86
  42. package/local.archive.org.key +27 -27
  43. package/package.json +121 -120
  44. package/renovate.json +6 -6
  45. package/src/collection-browser.ts +3070 -3070
  46. package/src/collection-facets/facets-template.ts +5 -0
  47. package/src/collection-facets/more-facets-content.ts +566 -96
  48. package/src/collection-facets/more-facets-pagination.ts +80 -9
  49. package/src/collection-facets/toggle-switch.ts +1 -0
  50. package/src/data-source/collection-browser-data-source.ts +1444 -1444
  51. package/src/data-source/collection-browser-query-state.ts +60 -60
  52. package/src/sort-filter-bar/sort-filter-bar.ts +733 -733
  53. package/test/collection-browser.test.ts +2402 -2402
  54. package/test/collection-facets/more-facets-content.test.ts +251 -4
  55. package/test/collection-facets/more-facets-pagination.test.ts +87 -3
  56. package/test/mocks/mock-search-responses.ts +48 -0
  57. package/test/mocks/mock-search-service.ts +2 -0
  58. package/test/sort-filter-bar/sort-filter-bar.test.ts +443 -443
  59. package/tsconfig.json +25 -25
  60. package/web-dev-server.config.mjs +30 -30
  61. package/web-test-runner.config.mjs +52 -52
  62. package/.claude/settings.local.json +0 -8
@@ -1,443 +1,443 @@
1
- import { expect, fixture } from '@open-wc/testing';
2
- import sinon from 'sinon';
3
- import { html } from 'lit';
4
- import type { IaDropdown } from '@internetarchive/ia-dropdown';
5
- import type { SortFilterBar } from '../../src/sort-filter-bar/sort-filter-bar';
6
- import { SortField, defaultSortAvailability } from '../../src/models';
7
-
8
- import '../../src/sort-filter-bar/sort-filter-bar';
9
-
10
- describe('Sort dropdown behavior', () => {
11
- let el: SortFilterBar;
12
- let sortDropdown: IaDropdown;
13
-
14
- beforeEach(async () => {
15
- el = await fixture<SortFilterBar>(html`
16
- <sort-filter-bar></sort-filter-bar>
17
- `);
18
- sortDropdown = el.shadowRoot?.querySelector('#sort-dropdown') as IaDropdown;
19
- await el.updateComplete;
20
- });
21
-
22
- it('should render basic component', async () => {
23
- const sortSelectorContainer = el.shadowRoot?.querySelector(
24
- '#sort-selector-container',
25
- );
26
- expect(sortSelectorContainer).to.exist;
27
- });
28
-
29
- it('should render sort-by label', async () => {
30
- const sortByLabel = el.shadowRoot?.querySelector('.sort-by-text');
31
- expect(sortByLabel).to.exist;
32
- expect(sortByLabel?.textContent?.trim()).to.equal('Sort by:');
33
- });
34
-
35
- it('should render sort direction button', async () => {
36
- const sortDirections = el.shadowRoot?.querySelector(
37
- '.sort-direction-container',
38
- );
39
- expect(sortDirections).to.exist;
40
- expect(sortDirections?.querySelector('.sort-direction-icon')).to.exist;
41
- });
42
-
43
- it('renders default set of sort options in dropdown', async () => {
44
- expect(sortDropdown).to.exist;
45
- expect(sortDropdown.options.map(o => o.id)).to.deep.equal([
46
- SortField.relevance,
47
- SortField.alltimeview,
48
- SortField.weeklyview,
49
- SortField.title,
50
- SortField.date,
51
- SortField.datearchived,
52
- SortField.datereviewed,
53
- SortField.dateadded,
54
- SortField.creator,
55
- ]);
56
- });
57
-
58
- it('respects overridden sort field availability', async () => {
59
- const customSortAvailability: Record<SortField, boolean> = {
60
- ...defaultSortAvailability,
61
- [SortField.title]: false,
62
- [SortField.datearchived]: false,
63
- [SortField.datereviewed]: false,
64
- [SortField.creator]: false,
65
- };
66
-
67
- el.sortFieldAvailability = customSortAvailability;
68
- await el.updateComplete;
69
-
70
- const dropdown = el.shadowRoot?.querySelector(
71
- '#sort-dropdown',
72
- ) as IaDropdown;
73
- expect(dropdown.options.length).to.equal(5);
74
- expect(dropdown.options.map(o => o.id)).to.deep.equal([
75
- SortField.relevance,
76
- SortField.alltimeview,
77
- SortField.weeklyview,
78
- SortField.date,
79
- SortField.dateadded,
80
- ]);
81
- });
82
-
83
- it('shows the display name of the selected sort', async () => {
84
- el.selectedSort = SortField.alltimeview;
85
- await el.updateComplete;
86
-
87
- const label = sortDropdown?.querySelector('.dropdown-label');
88
- expect(label?.textContent?.trim()).to.equal('All-time views');
89
- });
90
-
91
- it('falls back to first available sort when default is unavailable', async () => {
92
- el.selectedSort = SortField.default;
93
- el.defaultSortField = SortField.relevance;
94
- el.sortFieldAvailability = {
95
- ...defaultSortAvailability,
96
- [SortField.relevance]: false,
97
- };
98
- await el.updateComplete;
99
-
100
- const label = sortDropdown?.querySelector('.dropdown-label');
101
- // No relevance, so fall back to the first one in the list
102
- expect(label?.textContent?.trim()).to.equal('All-time views');
103
- });
104
-
105
- it('changes selected sort when dropdown option selected', async () => {
106
- expect(sortDropdown).to.exist;
107
-
108
- sortDropdown.selectedOption = 'title';
109
- const option = { id: 'title' };
110
- sortDropdown.dispatchEvent(
111
- new CustomEvent('optionSelected', { detail: { option } }),
112
- );
113
- await el.updateComplete;
114
-
115
- expect(el.selectedSort).to.equal('title');
116
- });
117
-
118
- it('selects a sort option by clicking it in the dropdown', async () => {
119
- el.selectedSort = SortField.title;
120
- await el.updateComplete;
121
-
122
- const dropdown = el.shadowRoot?.querySelector(
123
- '#sort-dropdown',
124
- ) as IaDropdown;
125
- expect(dropdown).to.exist;
126
-
127
- const firstOption = dropdown?.shadowRoot?.querySelector(
128
- 'li > button',
129
- ) as HTMLButtonElement;
130
- expect(firstOption).to.exist;
131
-
132
- firstOption?.click();
133
- await el.updateComplete;
134
-
135
- // The first option is relevance by default
136
- expect(el.selectedSort).to.equal(SortField.relevance);
137
- });
138
-
139
- it('emits sortChanged event when sort option selected', async () => {
140
- const sortChangedHandler = sinon.spy();
141
- el.addEventListener('sortChanged', sortChangedHandler);
142
-
143
- const option = { id: SortField.title };
144
- sortDropdown.dispatchEvent(
145
- new CustomEvent('optionSelected', { detail: { option } }),
146
- );
147
- await el.updateComplete;
148
-
149
- expect(sortChangedHandler.calledOnce).to.be.true;
150
- const eventDetail = sortChangedHandler.firstCall.args[0].detail;
151
- expect(eventDetail.selectedSort).to.equal(SortField.title);
152
- expect(eventDetail.sortDirection).to.equal('asc');
153
- });
154
-
155
- it('renders sort selector backdrop when dropdown is open', async () => {
156
- expect(sortDropdown).to.exist;
157
-
158
- const caret = sortDropdown?.shadowRoot?.querySelector(
159
- '.caret',
160
- ) as HTMLElement;
161
- expect(caret).to.exist;
162
-
163
- caret!.click();
164
- await el.updateComplete;
165
-
166
- let backdrop = el.shadowRoot?.querySelector(
167
- '#sort-selector-backdrop',
168
- ) as HTMLElement;
169
- expect(backdrop).to.exist;
170
-
171
- // Clicking the backdrop should close the dropdown
172
- backdrop!.click();
173
- await el.updateComplete;
174
-
175
- backdrop = el.shadowRoot?.querySelector(
176
- '#sort-selector-backdrop',
177
- ) as HTMLElement;
178
- expect(backdrop).not.to.exist;
179
- });
180
-
181
- it('pressing Escape key closes the dropdown', async () => {
182
- expect(sortDropdown).to.exist;
183
-
184
- const caret = sortDropdown?.shadowRoot?.querySelector(
185
- '.caret',
186
- ) as HTMLElement;
187
- expect(caret).to.exist;
188
-
189
- caret!.click();
190
- await el.updateComplete;
191
-
192
- let backdrop = el.shadowRoot?.querySelector(
193
- '#sort-selector-backdrop',
194
- ) as HTMLElement;
195
- expect(backdrop).to.exist;
196
-
197
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
198
- await el.updateComplete;
199
-
200
- backdrop = el.shadowRoot?.querySelector(
201
- '#sort-selector-backdrop',
202
- ) as HTMLElement;
203
- expect(backdrop).not.to.exist;
204
- });
205
-
206
- it('clears title filter when sort changed from title', async () => {
207
- el.selectedSort = 'title' as SortField;
208
- el.selectedTitleFilter = 'A';
209
- await el.updateComplete;
210
-
211
- const dropdown = el.shadowRoot?.querySelector(
212
- '#sort-dropdown',
213
- ) as IaDropdown;
214
- expect(dropdown).to.exist;
215
-
216
- dropdown.selectedOption = 'relevance';
217
- const option = { id: 'relevance' };
218
- dropdown.dispatchEvent(
219
- new CustomEvent('optionSelected', { detail: { option } }),
220
- );
221
- await el.updateComplete;
222
-
223
- expect(el.selectedSort).to.equal('relevance');
224
- expect(el.selectedTitleFilter).to.be.null;
225
- });
226
-
227
- it('clears creator filter when sort changed from creator', async () => {
228
- el.selectedSort = 'creator' as SortField;
229
- el.selectedCreatorFilter = 'A';
230
- await el.updateComplete;
231
-
232
- const dropdown = el.shadowRoot?.querySelector(
233
- '#sort-dropdown',
234
- ) as IaDropdown;
235
- expect(dropdown).to.exist;
236
-
237
- dropdown.selectedOption = 'relevance';
238
- const option = { id: 'relevance' };
239
- dropdown.dispatchEvent(
240
- new CustomEvent('optionSelected', { detail: { option } }),
241
- );
242
- await el.updateComplete;
243
-
244
- expect(el.selectedSort).to.equal('relevance');
245
- expect(el.selectedCreatorFilter).to.be.null;
246
- });
247
-
248
- it('contains sort-options slot when enabled', async () => {
249
- const slotEl = await fixture<SortFilterBar>(html`
250
- <sort-filter-bar .enableSortOptionsSlot=${true}></sort-filter-bar>
251
- `);
252
- await slotEl.updateComplete;
253
-
254
- const sortOptionsSlot = slotEl?.shadowRoot?.querySelector(
255
- 'slot[name="sort-options"]',
256
- );
257
- expect(sortOptionsSlot).to.exist;
258
-
259
- expect(slotEl?.shadowRoot?.querySelector('#sort-selector-container')).to.not
260
- .exist;
261
- });
262
- });
263
-
264
- describe('Sort direction button behavior', () => {
265
- it('should disable sort direction button when sorting by relevance', async () => {
266
- const el = await fixture<SortFilterBar>(html`
267
- <sort-filter-bar> </sort-filter-bar>
268
- `);
269
-
270
- el.selectedSort = 'relevance' as SortField;
271
- await el.updateComplete;
272
-
273
- const sortDirectionButton = el.shadowRoot?.querySelector(
274
- '.sort-direction-selector',
275
- ) as HTMLButtonElement;
276
- expect(sortDirectionButton).to.exist;
277
- expect(sortDirectionButton.disabled).to.be.true;
278
- });
279
-
280
- it('should enable sort direction button when not sorting by relevance', async () => {
281
- const el = await fixture<SortFilterBar>(html`
282
- <sort-filter-bar> </sort-filter-bar>
283
- `);
284
-
285
- el.selectedSort = 'title' as SortField;
286
- await el.updateComplete;
287
-
288
- const sortDirectionButton = el.shadowRoot?.querySelector(
289
- '.sort-direction-selector',
290
- ) as HTMLButtonElement;
291
- expect(sortDirectionButton).to.exist;
292
- expect(sortDirectionButton.disabled).to.be.false;
293
- });
294
-
295
- it('should toggle sort direction when clicked', async () => {
296
- const el = await fixture<SortFilterBar>(html`
297
- <sort-filter-bar> </sort-filter-bar>
298
- `);
299
-
300
- el.selectedSort = 'title' as SortField;
301
- el.sortDirection = 'asc';
302
- await el.updateComplete;
303
-
304
- const sortDirectionButton = el.shadowRoot?.querySelector(
305
- '.sort-direction-selector',
306
- ) as HTMLButtonElement;
307
-
308
- sortDirectionButton.click();
309
- await el.updateComplete;
310
- expect(el.sortDirection).to.equal('desc');
311
-
312
- sortDirectionButton.click();
313
- await el.updateComplete;
314
- expect(el.sortDirection).to.equal('asc');
315
- });
316
- });
317
-
318
- describe('Display mode/style buttons', () => {
319
- it('should render all display mode buttons', async () => {
320
- const el = await fixture<SortFilterBar>(html`
321
- <sort-filter-bar> </sort-filter-bar>
322
- `);
323
-
324
- const displayModeButtonList = el.shadowRoot
325
- ?.querySelector('#display-style-selector')
326
- ?.querySelector('ul');
327
-
328
- const gridButton = displayModeButtonList?.children
329
- .item(0)
330
- ?.querySelector('#grid-button');
331
- expect(gridButton).to.exist;
332
-
333
- const detailListButton = displayModeButtonList?.children
334
- .item(1)
335
- ?.querySelector('#list-detail-button');
336
- expect(detailListButton).to.exist;
337
-
338
- const compactListButton = displayModeButtonList?.children
339
- .item(2)
340
- ?.querySelector('#list-compact-button');
341
- expect(compactListButton).to.exist;
342
- });
343
-
344
- it('should not render display mode buttons when suppressed', async () => {
345
- const el = await fixture<SortFilterBar>(html`
346
- <sort-filter-bar suppressDisplayModes></sort-filter-bar>
347
- `);
348
-
349
- const displayModeButtonList = el.shadowRoot?.querySelector(
350
- '#display-style-selector',
351
- );
352
- expect(displayModeButtonList).not.to.exist;
353
- });
354
-
355
- it('should active current display mode', async () => {
356
- const el = await fixture<SortFilterBar>(html`
357
- <sort-filter-bar> </sort-filter-bar>
358
- `);
359
-
360
- el.displayMode = 'grid';
361
- await el.updateComplete;
362
-
363
- const displayModeTitle = el.shadowRoot
364
- ?.querySelector('#display-style-selector')
365
- ?.querySelector('button.active')
366
- ?.getAttribute('title');
367
- expect(displayModeTitle).to.equal('Tile view');
368
- });
369
-
370
- it('should change displayMode prop to the one clicked', async () => {
371
- const el = await fixture<SortFilterBar>(html`
372
- <sort-filter-bar> </sort-filter-bar>
373
- `);
374
-
375
- el.displayMode = 'grid';
376
- await el.updateComplete;
377
-
378
- const extendedListButton = el.shadowRoot?.querySelector(
379
- '#list-detail-button',
380
- ) as HTMLElement;
381
- extendedListButton.click();
382
- await el.updateComplete;
383
- expect(el.displayMode).to.equal('list-detail');
384
-
385
- const compactListButton = el.shadowRoot?.querySelector(
386
- '#list-compact-button',
387
- ) as HTMLElement;
388
- compactListButton.click();
389
- await el.updateComplete;
390
- expect(el.displayMode).to.equal('list-compact');
391
-
392
- const gridModeButton = el.shadowRoot?.querySelector(
393
- '#grid-button',
394
- ) as HTMLElement;
395
- gridModeButton.click();
396
- await el.updateComplete;
397
- expect(el.displayMode).to.equal('grid');
398
- });
399
- });
400
-
401
- describe('Sort/filter bar letter behavior', () => {
402
- it('sets the selected title letter when clicked', async () => {
403
- const el = await fixture<SortFilterBar>(html`
404
- <sort-filter-bar></sort-filter-bar>
405
- `);
406
-
407
- el.selectedSort = 'title' as SortField;
408
- el.prefixFilterCountMap = { title: { T: 1 }, creator: {} };
409
- await el.updateComplete;
410
-
411
- const alphaBar = el.shadowRoot?.querySelector('alpha-bar');
412
- const letterLink = alphaBar?.shadowRoot?.querySelector(
413
- 'li > button:not(:disabled)',
414
- ) as HTMLAnchorElement;
415
- expect(letterLink?.textContent?.trim()).to.equal('T');
416
-
417
- letterLink?.click();
418
- await el.updateComplete;
419
-
420
- expect(el.selectedTitleFilter).to.equal('T');
421
- });
422
-
423
- it('sets the selected creator letter when clicked', async () => {
424
- const el = await fixture<SortFilterBar>(html`
425
- <sort-filter-bar></sort-filter-bar>
426
- `);
427
-
428
- el.selectedSort = 'creator' as SortField;
429
- el.prefixFilterCountMap = { title: {}, creator: { C: 1 } };
430
- await el.updateComplete;
431
-
432
- const alphaBar = el.shadowRoot?.querySelector('alpha-bar');
433
- const letterLink = alphaBar?.shadowRoot?.querySelector(
434
- 'li > button:not(:disabled)',
435
- ) as HTMLAnchorElement;
436
- expect(letterLink?.textContent?.trim()).to.equal('C');
437
-
438
- letterLink?.click();
439
- await el.updateComplete;
440
-
441
- expect(el.selectedCreatorFilter).to.equal('C');
442
- });
443
- });
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import sinon from 'sinon';
3
+ import { html } from 'lit';
4
+ import type { IaDropdown } from '@internetarchive/ia-dropdown';
5
+ import type { SortFilterBar } from '../../src/sort-filter-bar/sort-filter-bar';
6
+ import { SortField, defaultSortAvailability } from '../../src/models';
7
+
8
+ import '../../src/sort-filter-bar/sort-filter-bar';
9
+
10
+ describe('Sort dropdown behavior', () => {
11
+ let el: SortFilterBar;
12
+ let sortDropdown: IaDropdown;
13
+
14
+ beforeEach(async () => {
15
+ el = await fixture<SortFilterBar>(html`
16
+ <sort-filter-bar></sort-filter-bar>
17
+ `);
18
+ sortDropdown = el.shadowRoot?.querySelector('#sort-dropdown') as IaDropdown;
19
+ await el.updateComplete;
20
+ });
21
+
22
+ it('should render basic component', async () => {
23
+ const sortSelectorContainer = el.shadowRoot?.querySelector(
24
+ '#sort-selector-container',
25
+ );
26
+ expect(sortSelectorContainer).to.exist;
27
+ });
28
+
29
+ it('should render sort-by label', async () => {
30
+ const sortByLabel = el.shadowRoot?.querySelector('.sort-by-text');
31
+ expect(sortByLabel).to.exist;
32
+ expect(sortByLabel?.textContent?.trim()).to.equal('Sort by:');
33
+ });
34
+
35
+ it('should render sort direction button', async () => {
36
+ const sortDirections = el.shadowRoot?.querySelector(
37
+ '.sort-direction-container',
38
+ );
39
+ expect(sortDirections).to.exist;
40
+ expect(sortDirections?.querySelector('.sort-direction-icon')).to.exist;
41
+ });
42
+
43
+ it('renders default set of sort options in dropdown', async () => {
44
+ expect(sortDropdown).to.exist;
45
+ expect(sortDropdown.options.map(o => o.id)).to.deep.equal([
46
+ SortField.relevance,
47
+ SortField.alltimeview,
48
+ SortField.weeklyview,
49
+ SortField.title,
50
+ SortField.date,
51
+ SortField.datearchived,
52
+ SortField.datereviewed,
53
+ SortField.dateadded,
54
+ SortField.creator,
55
+ ]);
56
+ });
57
+
58
+ it('respects overridden sort field availability', async () => {
59
+ const customSortAvailability: Record<SortField, boolean> = {
60
+ ...defaultSortAvailability,
61
+ [SortField.title]: false,
62
+ [SortField.datearchived]: false,
63
+ [SortField.datereviewed]: false,
64
+ [SortField.creator]: false,
65
+ };
66
+
67
+ el.sortFieldAvailability = customSortAvailability;
68
+ await el.updateComplete;
69
+
70
+ const dropdown = el.shadowRoot?.querySelector(
71
+ '#sort-dropdown',
72
+ ) as IaDropdown;
73
+ expect(dropdown.options.length).to.equal(5);
74
+ expect(dropdown.options.map(o => o.id)).to.deep.equal([
75
+ SortField.relevance,
76
+ SortField.alltimeview,
77
+ SortField.weeklyview,
78
+ SortField.date,
79
+ SortField.dateadded,
80
+ ]);
81
+ });
82
+
83
+ it('shows the display name of the selected sort', async () => {
84
+ el.selectedSort = SortField.alltimeview;
85
+ await el.updateComplete;
86
+
87
+ const label = sortDropdown?.querySelector('.dropdown-label');
88
+ expect(label?.textContent?.trim()).to.equal('All-time views');
89
+ });
90
+
91
+ it('falls back to first available sort when default is unavailable', async () => {
92
+ el.selectedSort = SortField.default;
93
+ el.defaultSortField = SortField.relevance;
94
+ el.sortFieldAvailability = {
95
+ ...defaultSortAvailability,
96
+ [SortField.relevance]: false,
97
+ };
98
+ await el.updateComplete;
99
+
100
+ const label = sortDropdown?.querySelector('.dropdown-label');
101
+ // No relevance, so fall back to the first one in the list
102
+ expect(label?.textContent?.trim()).to.equal('All-time views');
103
+ });
104
+
105
+ it('changes selected sort when dropdown option selected', async () => {
106
+ expect(sortDropdown).to.exist;
107
+
108
+ sortDropdown.selectedOption = 'title';
109
+ const option = { id: 'title' };
110
+ sortDropdown.dispatchEvent(
111
+ new CustomEvent('optionSelected', { detail: { option } }),
112
+ );
113
+ await el.updateComplete;
114
+
115
+ expect(el.selectedSort).to.equal('title');
116
+ });
117
+
118
+ it('selects a sort option by clicking it in the dropdown', async () => {
119
+ el.selectedSort = SortField.title;
120
+ await el.updateComplete;
121
+
122
+ const dropdown = el.shadowRoot?.querySelector(
123
+ '#sort-dropdown',
124
+ ) as IaDropdown;
125
+ expect(dropdown).to.exist;
126
+
127
+ const firstOption = dropdown?.shadowRoot?.querySelector(
128
+ 'li > button',
129
+ ) as HTMLButtonElement;
130
+ expect(firstOption).to.exist;
131
+
132
+ firstOption?.click();
133
+ await el.updateComplete;
134
+
135
+ // The first option is relevance by default
136
+ expect(el.selectedSort).to.equal(SortField.relevance);
137
+ });
138
+
139
+ it('emits sortChanged event when sort option selected', async () => {
140
+ const sortChangedHandler = sinon.spy();
141
+ el.addEventListener('sortChanged', sortChangedHandler);
142
+
143
+ const option = { id: SortField.title };
144
+ sortDropdown.dispatchEvent(
145
+ new CustomEvent('optionSelected', { detail: { option } }),
146
+ );
147
+ await el.updateComplete;
148
+
149
+ expect(sortChangedHandler.calledOnce).to.be.true;
150
+ const eventDetail = sortChangedHandler.firstCall.args[0].detail;
151
+ expect(eventDetail.selectedSort).to.equal(SortField.title);
152
+ expect(eventDetail.sortDirection).to.equal('asc');
153
+ });
154
+
155
+ it('renders sort selector backdrop when dropdown is open', async () => {
156
+ expect(sortDropdown).to.exist;
157
+
158
+ const caret = sortDropdown?.shadowRoot?.querySelector(
159
+ '.caret',
160
+ ) as HTMLElement;
161
+ expect(caret).to.exist;
162
+
163
+ caret!.click();
164
+ await el.updateComplete;
165
+
166
+ let backdrop = el.shadowRoot?.querySelector(
167
+ '#sort-selector-backdrop',
168
+ ) as HTMLElement;
169
+ expect(backdrop).to.exist;
170
+
171
+ // Clicking the backdrop should close the dropdown
172
+ backdrop!.click();
173
+ await el.updateComplete;
174
+
175
+ backdrop = el.shadowRoot?.querySelector(
176
+ '#sort-selector-backdrop',
177
+ ) as HTMLElement;
178
+ expect(backdrop).not.to.exist;
179
+ });
180
+
181
+ it('pressing Escape key closes the dropdown', async () => {
182
+ expect(sortDropdown).to.exist;
183
+
184
+ const caret = sortDropdown?.shadowRoot?.querySelector(
185
+ '.caret',
186
+ ) as HTMLElement;
187
+ expect(caret).to.exist;
188
+
189
+ caret!.click();
190
+ await el.updateComplete;
191
+
192
+ let backdrop = el.shadowRoot?.querySelector(
193
+ '#sort-selector-backdrop',
194
+ ) as HTMLElement;
195
+ expect(backdrop).to.exist;
196
+
197
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
198
+ await el.updateComplete;
199
+
200
+ backdrop = el.shadowRoot?.querySelector(
201
+ '#sort-selector-backdrop',
202
+ ) as HTMLElement;
203
+ expect(backdrop).not.to.exist;
204
+ });
205
+
206
+ it('clears title filter when sort changed from title', async () => {
207
+ el.selectedSort = 'title' as SortField;
208
+ el.selectedTitleFilter = 'A';
209
+ await el.updateComplete;
210
+
211
+ const dropdown = el.shadowRoot?.querySelector(
212
+ '#sort-dropdown',
213
+ ) as IaDropdown;
214
+ expect(dropdown).to.exist;
215
+
216
+ dropdown.selectedOption = 'relevance';
217
+ const option = { id: 'relevance' };
218
+ dropdown.dispatchEvent(
219
+ new CustomEvent('optionSelected', { detail: { option } }),
220
+ );
221
+ await el.updateComplete;
222
+
223
+ expect(el.selectedSort).to.equal('relevance');
224
+ expect(el.selectedTitleFilter).to.be.null;
225
+ });
226
+
227
+ it('clears creator filter when sort changed from creator', async () => {
228
+ el.selectedSort = 'creator' as SortField;
229
+ el.selectedCreatorFilter = 'A';
230
+ await el.updateComplete;
231
+
232
+ const dropdown = el.shadowRoot?.querySelector(
233
+ '#sort-dropdown',
234
+ ) as IaDropdown;
235
+ expect(dropdown).to.exist;
236
+
237
+ dropdown.selectedOption = 'relevance';
238
+ const option = { id: 'relevance' };
239
+ dropdown.dispatchEvent(
240
+ new CustomEvent('optionSelected', { detail: { option } }),
241
+ );
242
+ await el.updateComplete;
243
+
244
+ expect(el.selectedSort).to.equal('relevance');
245
+ expect(el.selectedCreatorFilter).to.be.null;
246
+ });
247
+
248
+ it('contains sort-options slot when enabled', async () => {
249
+ const slotEl = await fixture<SortFilterBar>(html`
250
+ <sort-filter-bar .enableSortOptionsSlot=${true}></sort-filter-bar>
251
+ `);
252
+ await slotEl.updateComplete;
253
+
254
+ const sortOptionsSlot = slotEl?.shadowRoot?.querySelector(
255
+ 'slot[name="sort-options"]',
256
+ );
257
+ expect(sortOptionsSlot).to.exist;
258
+
259
+ expect(slotEl?.shadowRoot?.querySelector('#sort-selector-container')).to.not
260
+ .exist;
261
+ });
262
+ });
263
+
264
+ describe('Sort direction button behavior', () => {
265
+ it('should disable sort direction button when sorting by relevance', async () => {
266
+ const el = await fixture<SortFilterBar>(html`
267
+ <sort-filter-bar> </sort-filter-bar>
268
+ `);
269
+
270
+ el.selectedSort = 'relevance' as SortField;
271
+ await el.updateComplete;
272
+
273
+ const sortDirectionButton = el.shadowRoot?.querySelector(
274
+ '.sort-direction-selector',
275
+ ) as HTMLButtonElement;
276
+ expect(sortDirectionButton).to.exist;
277
+ expect(sortDirectionButton.disabled).to.be.true;
278
+ });
279
+
280
+ it('should enable sort direction button when not sorting by relevance', async () => {
281
+ const el = await fixture<SortFilterBar>(html`
282
+ <sort-filter-bar> </sort-filter-bar>
283
+ `);
284
+
285
+ el.selectedSort = 'title' as SortField;
286
+ await el.updateComplete;
287
+
288
+ const sortDirectionButton = el.shadowRoot?.querySelector(
289
+ '.sort-direction-selector',
290
+ ) as HTMLButtonElement;
291
+ expect(sortDirectionButton).to.exist;
292
+ expect(sortDirectionButton.disabled).to.be.false;
293
+ });
294
+
295
+ it('should toggle sort direction when clicked', async () => {
296
+ const el = await fixture<SortFilterBar>(html`
297
+ <sort-filter-bar> </sort-filter-bar>
298
+ `);
299
+
300
+ el.selectedSort = 'title' as SortField;
301
+ el.sortDirection = 'asc';
302
+ await el.updateComplete;
303
+
304
+ const sortDirectionButton = el.shadowRoot?.querySelector(
305
+ '.sort-direction-selector',
306
+ ) as HTMLButtonElement;
307
+
308
+ sortDirectionButton.click();
309
+ await el.updateComplete;
310
+ expect(el.sortDirection).to.equal('desc');
311
+
312
+ sortDirectionButton.click();
313
+ await el.updateComplete;
314
+ expect(el.sortDirection).to.equal('asc');
315
+ });
316
+ });
317
+
318
+ describe('Display mode/style buttons', () => {
319
+ it('should render all display mode buttons', async () => {
320
+ const el = await fixture<SortFilterBar>(html`
321
+ <sort-filter-bar> </sort-filter-bar>
322
+ `);
323
+
324
+ const displayModeButtonList = el.shadowRoot
325
+ ?.querySelector('#display-style-selector')
326
+ ?.querySelector('ul');
327
+
328
+ const gridButton = displayModeButtonList?.children
329
+ .item(0)
330
+ ?.querySelector('#grid-button');
331
+ expect(gridButton).to.exist;
332
+
333
+ const detailListButton = displayModeButtonList?.children
334
+ .item(1)
335
+ ?.querySelector('#list-detail-button');
336
+ expect(detailListButton).to.exist;
337
+
338
+ const compactListButton = displayModeButtonList?.children
339
+ .item(2)
340
+ ?.querySelector('#list-compact-button');
341
+ expect(compactListButton).to.exist;
342
+ });
343
+
344
+ it('should not render display mode buttons when suppressed', async () => {
345
+ const el = await fixture<SortFilterBar>(html`
346
+ <sort-filter-bar suppressDisplayModes></sort-filter-bar>
347
+ `);
348
+
349
+ const displayModeButtonList = el.shadowRoot?.querySelector(
350
+ '#display-style-selector',
351
+ );
352
+ expect(displayModeButtonList).not.to.exist;
353
+ });
354
+
355
+ it('should active current display mode', async () => {
356
+ const el = await fixture<SortFilterBar>(html`
357
+ <sort-filter-bar> </sort-filter-bar>
358
+ `);
359
+
360
+ el.displayMode = 'grid';
361
+ await el.updateComplete;
362
+
363
+ const displayModeTitle = el.shadowRoot
364
+ ?.querySelector('#display-style-selector')
365
+ ?.querySelector('button.active')
366
+ ?.getAttribute('title');
367
+ expect(displayModeTitle).to.equal('Tile view');
368
+ });
369
+
370
+ it('should change displayMode prop to the one clicked', async () => {
371
+ const el = await fixture<SortFilterBar>(html`
372
+ <sort-filter-bar> </sort-filter-bar>
373
+ `);
374
+
375
+ el.displayMode = 'grid';
376
+ await el.updateComplete;
377
+
378
+ const extendedListButton = el.shadowRoot?.querySelector(
379
+ '#list-detail-button',
380
+ ) as HTMLElement;
381
+ extendedListButton.click();
382
+ await el.updateComplete;
383
+ expect(el.displayMode).to.equal('list-detail');
384
+
385
+ const compactListButton = el.shadowRoot?.querySelector(
386
+ '#list-compact-button',
387
+ ) as HTMLElement;
388
+ compactListButton.click();
389
+ await el.updateComplete;
390
+ expect(el.displayMode).to.equal('list-compact');
391
+
392
+ const gridModeButton = el.shadowRoot?.querySelector(
393
+ '#grid-button',
394
+ ) as HTMLElement;
395
+ gridModeButton.click();
396
+ await el.updateComplete;
397
+ expect(el.displayMode).to.equal('grid');
398
+ });
399
+ });
400
+
401
+ describe('Sort/filter bar letter behavior', () => {
402
+ it('sets the selected title letter when clicked', async () => {
403
+ const el = await fixture<SortFilterBar>(html`
404
+ <sort-filter-bar></sort-filter-bar>
405
+ `);
406
+
407
+ el.selectedSort = 'title' as SortField;
408
+ el.prefixFilterCountMap = { title: { T: 1 }, creator: {} };
409
+ await el.updateComplete;
410
+
411
+ const alphaBar = el.shadowRoot?.querySelector('alpha-bar');
412
+ const letterLink = alphaBar?.shadowRoot?.querySelector(
413
+ 'li > button:not(:disabled)',
414
+ ) as HTMLAnchorElement;
415
+ expect(letterLink?.textContent?.trim()).to.equal('T');
416
+
417
+ letterLink?.click();
418
+ await el.updateComplete;
419
+
420
+ expect(el.selectedTitleFilter).to.equal('T');
421
+ });
422
+
423
+ it('sets the selected creator letter when clicked', async () => {
424
+ const el = await fixture<SortFilterBar>(html`
425
+ <sort-filter-bar></sort-filter-bar>
426
+ `);
427
+
428
+ el.selectedSort = 'creator' as SortField;
429
+ el.prefixFilterCountMap = { title: {}, creator: { C: 1 } };
430
+ await el.updateComplete;
431
+
432
+ const alphaBar = el.shadowRoot?.querySelector('alpha-bar');
433
+ const letterLink = alphaBar?.shadowRoot?.querySelector(
434
+ 'li > button:not(:disabled)',
435
+ ) as HTMLAnchorElement;
436
+ expect(letterLink?.textContent?.trim()).to.equal('C');
437
+
438
+ letterLink?.click();
439
+ await el.updateComplete;
440
+
441
+ expect(el.selectedCreatorFilter).to.equal('C');
442
+ });
443
+ });