@internetarchive/collection-browser 0.4.16-alpha.9 → 0.4.17

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 (74) hide show
  1. package/dist/src/app-root.js +12 -0
  2. package/dist/src/app-root.js.map +1 -1
  3. package/dist/src/collection-browser.d.ts +53 -14
  4. package/dist/src/collection-browser.js +279 -113
  5. package/dist/src/collection-browser.js.map +1 -1
  6. package/dist/src/collection-facets/facets-template.d.ts +3 -0
  7. package/dist/src/collection-facets/facets-template.js +20 -1
  8. package/dist/src/collection-facets/facets-template.js.map +1 -1
  9. package/dist/src/collection-facets/more-facets-content.js +7 -4
  10. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  11. package/dist/src/collection-facets.d.ts +0 -2
  12. package/dist/src/collection-facets.js +1 -4
  13. package/dist/src/collection-facets.js.map +1 -1
  14. package/dist/src/empty-placeholder.js +1 -0
  15. package/dist/src/empty-placeholder.js.map +1 -1
  16. package/dist/src/models.d.ts +5 -0
  17. package/dist/src/models.js.map +1 -1
  18. package/dist/src/tiles/grid/item-tile.js +10 -3
  19. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  20. package/dist/src/tiles/list/tile-list-compact.d.ts +1 -0
  21. package/dist/src/tiles/list/tile-list-compact.js +13 -1
  22. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  23. package/dist/src/tiles/list/tile-list.js +10 -1
  24. package/dist/src/tiles/list/tile-list.js.map +1 -1
  25. package/dist/src/utils/format-date.d.ts +1 -1
  26. package/dist/src/utils/format-date.js +3 -0
  27. package/dist/src/utils/format-date.js.map +1 -1
  28. package/dist/src/utils/local-date-from-utc.d.ts +9 -0
  29. package/dist/src/utils/local-date-from-utc.js +16 -0
  30. package/dist/src/utils/local-date-from-utc.js.map +1 -0
  31. package/dist/test/collection-browser.test.js +73 -10
  32. package/dist/test/collection-browser.test.js.map +1 -1
  33. package/dist/test/collection-facets/facets-template.test.js +80 -0
  34. package/dist/test/collection-facets/facets-template.test.js.map +1 -1
  35. package/dist/test/mocks/mock-collection-name-cache.d.ts +2 -0
  36. package/dist/test/mocks/mock-collection-name-cache.js +4 -0
  37. package/dist/test/mocks/mock-collection-name-cache.js.map +1 -1
  38. package/dist/test/mocks/mock-search-responses.d.ts +2 -0
  39. package/dist/test/mocks/mock-search-responses.js +81 -0
  40. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  41. package/dist/test/mocks/mock-search-service.js +3 -1
  42. package/dist/test/mocks/mock-search-service.js.map +1 -1
  43. package/dist/test/tiles/grid/item-tile.test.js +124 -3
  44. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  45. package/dist/test/tiles/list/tile-list-compact.test.js +65 -20
  46. package/dist/test/tiles/list/tile-list-compact.test.js.map +1 -1
  47. package/dist/test/tiles/list/tile-list.test.js +106 -4
  48. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  49. package/dist/test/utils/local-date-from-utc.test.d.ts +1 -0
  50. package/dist/test/utils/local-date-from-utc.test.js +27 -0
  51. package/dist/test/utils/local-date-from-utc.test.js.map +1 -0
  52. package/index.html +1 -0
  53. package/package.json +3 -3
  54. package/src/app-root.ts +12 -0
  55. package/src/collection-browser.ts +311 -114
  56. package/src/collection-facets/facets-template.ts +32 -1
  57. package/src/collection-facets/more-facets-content.ts +4 -1
  58. package/src/collection-facets.ts +1 -8
  59. package/src/empty-placeholder.ts +1 -0
  60. package/src/models.ts +6 -0
  61. package/src/tiles/grid/item-tile.ts +11 -4
  62. package/src/tiles/list/tile-list-compact.ts +16 -2
  63. package/src/tiles/list/tile-list.ts +12 -5
  64. package/src/utils/format-date.ts +4 -0
  65. package/src/utils/local-date-from-utc.ts +15 -0
  66. package/test/collection-browser.test.ts +101 -12
  67. package/test/collection-facets/facets-template.test.ts +98 -0
  68. package/test/mocks/mock-collection-name-cache.ts +8 -0
  69. package/test/mocks/mock-search-responses.ts +89 -0
  70. package/test/mocks/mock-search-service.ts +4 -0
  71. package/test/tiles/grid/item-tile.test.ts +145 -3
  72. package/test/tiles/list/tile-list-compact.test.ts +70 -19
  73. package/test/tiles/list/tile-list.test.ts +118 -4
  74. package/test/utils/local-date-from-utc.test.ts +37 -0
@@ -10,6 +10,8 @@ import {
10
10
  FacetBucket,
11
11
  SelectedFacets,
12
12
  getDefaultSelectedFacets,
13
+ FacetEventDetails,
14
+ FacetState,
13
15
  } from '../models';
14
16
 
15
17
  @customElement('facets-template')
@@ -31,6 +33,12 @@ export class FacetsTemplate extends LitElement {
31
33
  } else {
32
34
  this.facetUnchecked(name as FacetOption, value);
33
35
  }
36
+
37
+ this.dispatchFacetClickEvent(
38
+ name as FacetOption,
39
+ this.getFacetState(checked, negative),
40
+ negative
41
+ );
34
42
  }
35
43
 
36
44
  private facetChecked(
@@ -49,7 +57,7 @@ export class FacetsTemplate extends LitElement {
49
57
  newFacets = getDefaultSelectedFacets();
50
58
  }
51
59
  newFacets[key][value] = {
52
- state: negative ? 'hidden' : 'selected',
60
+ state: this.getFacetState(true, negative),
53
61
  count,
54
62
  } as FacetBucket;
55
63
 
@@ -73,6 +81,29 @@ export class FacetsTemplate extends LitElement {
73
81
  this.dispatchSelectedFacetsChanged();
74
82
  }
75
83
 
84
+ /** Returns the composed facet state corresponding to a positive or negative facet's checked state */
85
+ private getFacetState(checked: boolean, negative: boolean): FacetState {
86
+ let state: FacetState;
87
+ if (checked) {
88
+ state = negative ? 'hidden' : 'selected';
89
+ } else {
90
+ state = 'none';
91
+ }
92
+ return state;
93
+ }
94
+
95
+ private dispatchFacetClickEvent(
96
+ key: FacetOption,
97
+ state: FacetState,
98
+ negative: boolean
99
+ ) {
100
+ const event = new CustomEvent<FacetEventDetails>('facetClick', {
101
+ detail: { key, state, negative },
102
+ composed: true,
103
+ });
104
+ this.dispatchEvent(event);
105
+ }
106
+
76
107
  private dispatchSelectedFacetsChanged() {
77
108
  const event = new CustomEvent<SelectedFacets>('selectedFacetsChanged', {
78
109
  detail: this.selectedFacets,
@@ -130,13 +130,16 @@ export class MoreFacetsContent extends LitElement {
130
130
  * - this.aggregations - hold result of search service and being used for further processing.
131
131
  */
132
132
  async updateSpecificFacets(): Promise<void> {
133
+ const trimmedQuery = this.query?.trim();
134
+ if (!trimmedQuery) return;
135
+
133
136
  const aggregations = {
134
137
  simpleParams: [this.facetAggregationKey as string],
135
138
  };
136
139
  const aggregationsSize = 65535; // todo - do we want to have all the records at once?
137
140
 
138
141
  const params: SearchParams = {
139
- query: this.query as string,
142
+ query: trimmedQuery,
140
143
  filters: this.filterMap,
141
144
  aggregations,
142
145
  aggregationsSize,
@@ -99,13 +99,6 @@ export class CollectionFacets extends LitElement {
99
99
  @property({ type: Object, attribute: false })
100
100
  collectionNameCache?: CollectionNameCacheInterface;
101
101
 
102
- /** Fires when a facet is clicked */
103
- @property({ type: Function }) onFacetClick?: (
104
- name: FacetOption,
105
- facetChecked: boolean,
106
- negative: boolean
107
- ) => void;
108
-
109
102
  @state() openFacets: Record<FacetOption, boolean> = {
110
103
  subject: false,
111
104
  lending: false,
@@ -554,7 +547,7 @@ export class CollectionFacets extends LitElement {
554
547
  transform: rotate(90deg);
555
548
  }
556
549
 
557
- .facet-group {
550
+ .facet-group:not(:last-child) {
558
551
  margin-bottom: 2rem;
559
552
  }
560
553
 
@@ -119,6 +119,7 @@ export class EmptyPlaceholder extends LitElement {
119
119
 
120
120
  .error-details {
121
121
  font-size: 1.2rem;
122
+ word-break: break-word;
122
123
  }
123
124
  `;
124
125
  }
package/src/models.ts CHANGED
@@ -220,6 +220,12 @@ export interface FacetGroup {
220
220
  buckets: FacetBucket[];
221
221
  }
222
222
 
223
+ export type FacetEventDetails = {
224
+ key: FacetOption;
225
+ state: FacetState;
226
+ negative: boolean;
227
+ };
228
+
223
229
  export type FacetValue = string;
224
230
 
225
231
  export type SelectedFacets = Record<
@@ -11,7 +11,8 @@ import { customElement, property } from 'lit/decorators.js';
11
11
  import { ifDefined } from 'lit/directives/if-defined.js';
12
12
  import type { SortParam } from '@internetarchive/search-service';
13
13
 
14
- import { formatDate } from '../../utils/format-date';
14
+ import { DateFormat, formatDate } from '../../utils/format-date';
15
+ import { isFirstMillisecondOfUTCYear } from '../../utils/local-date-from-utc';
15
16
  import type { TileModel } from '../../models';
16
17
 
17
18
  import { baseTileStyles } from './styles/tile-grid-shared-styles';
@@ -104,10 +105,16 @@ export class ItemTile extends LitElement {
104
105
 
105
106
  private get sortedDateInfoTemplate() {
106
107
  let sortedValue;
108
+ let format: DateFormat = 'long';
107
109
  switch (this.sortParam?.field) {
108
- case 'date':
109
- sortedValue = { field: 'published', value: this.model?.datePublished };
110
+ case 'date': {
111
+ const datePublished = this.model?.datePublished;
112
+ sortedValue = { field: 'published', value: datePublished };
113
+ if (isFirstMillisecondOfUTCYear(datePublished)) {
114
+ format = 'year-only';
115
+ }
110
116
  break;
117
+ }
111
118
  case 'reviewdate':
112
119
  sortedValue = { field: 'reviewed', value: this.model?.dateReviewed };
113
120
  break;
@@ -127,7 +134,7 @@ export class ItemTile extends LitElement {
127
134
  return html`
128
135
  <div class="date-sorted-by truncated">
129
136
  <span>
130
- ${sortedValue?.field} ${formatDate(sortedValue?.value, 'long')}
137
+ ${sortedValue?.field} ${formatDate(sortedValue?.value, format)}
131
138
  </span>
132
139
  </div>
133
140
  `;
@@ -6,6 +6,7 @@ import type { TileModel } from '../../models';
6
6
 
7
7
  import { formatCount, NumberFormat } from '../../utils/format-count';
8
8
  import { formatDate, DateFormat } from '../../utils/format-date';
9
+ import { isFirstMillisecondOfUTCYear } from '../../utils/local-date-from-utc';
9
10
  import { accountLabel } from './account-label';
10
11
 
11
12
  import '../image-block';
@@ -49,7 +50,7 @@ export class TileListCompact extends LitElement {
49
50
  ? accountLabel(this.model?.dateAdded)
50
51
  : DOMPurify.sanitize(this.model?.creator ?? '')}
51
52
  </div>
52
- <div id="date">${formatDate(this.date, this.formatSize)}</div>
53
+ <div id="date">${formatDate(this.date, this.dateFormatSize)}</div>
53
54
  <div id="icon">
54
55
  <mediatype-icon
55
56
  .mediatype=${this.model?.mediatype}
@@ -111,7 +112,20 @@ export class TileListCompact extends LitElement {
111
112
  return 'desktop';
112
113
  }
113
114
 
114
- private get formatSize(): DateFormat | NumberFormat {
115
+ private get dateFormatSize(): DateFormat {
116
+ // If we're showing a date published of Jan 1 at midnight, only show the year.
117
+ // This is because items with only a year for their publication date are normalized to
118
+ // Jan 1 at midnight timestamps in the search engine documents.
119
+ if (
120
+ (!this.sortParam?.field || this.sortParam.field === 'date') && // No sort or date published
121
+ isFirstMillisecondOfUTCYear(this.model?.datePublished)
122
+ ) {
123
+ return 'year-only';
124
+ }
125
+ return this.formatSize;
126
+ }
127
+
128
+ private get formatSize(): NumberFormat {
115
129
  if (
116
130
  this.mobileBreakpoint &&
117
131
  this.currentWidth &&
@@ -21,6 +21,7 @@ import { dateLabel } from './date-label';
21
21
  import { accountLabel } from './account-label';
22
22
  import { formatCount, NumberFormat } from '../../utils/format-count';
23
23
  import { formatDate, DateFormat } from '../../utils/format-date';
24
+ import { isFirstMillisecondOfUTCYear } from '../../utils/local-date-from-utc';
24
25
 
25
26
  import '../image-block';
26
27
  import '../mediatype-icon';
@@ -205,10 +206,16 @@ export class TileList extends LitElement {
205
206
  }
206
207
 
207
208
  private get datePublishedTemplate() {
208
- return this.metadataTemplate(
209
- formatDate(this.model?.datePublished, 'long'),
210
- 'Published'
211
- );
209
+ // If we're showing a date published of Jan 1 at midnight, only show the year.
210
+ // This is because items with only a year for their publication date are normalized to
211
+ // Jan 1 at midnight timestamps in the search engine documents.
212
+ const date: Date | undefined = this.model?.datePublished;
213
+ let format: DateFormat = 'long';
214
+ if (isFirstMillisecondOfUTCYear(date)) {
215
+ format = 'year-only';
216
+ }
217
+
218
+ return this.metadataTemplate(formatDate(date, format), 'Published');
212
219
  }
213
220
 
214
221
  // Show date label/value when sorted by date type
@@ -430,7 +437,7 @@ export class TileList extends LitElement {
430
437
  return 'desktop';
431
438
  }
432
439
 
433
- private get formatSize(): DateFormat | NumberFormat {
440
+ private get formatSize(): NumberFormat {
434
441
  if (
435
442
  this.mobileBreakpoint &&
436
443
  this.currentWidth &&
@@ -3,6 +3,7 @@
3
3
  * Override browser timezone to always display same date as in data
4
4
  */
5
5
  export type DateFormat =
6
+ | 'year-only' // 2020
6
7
  | 'short' // Dec 2020
7
8
  | 'long'; // Dec 20, 2020
8
9
 
@@ -18,6 +19,9 @@ export function formatDate(
18
19
  timeZone: 'UTC', // Override browser timezone
19
20
  };
20
21
  switch (format) {
22
+ case 'year-only':
23
+ options.year = 'numeric';
24
+ break;
21
25
  case 'short':
22
26
  options.month = 'short';
23
27
  options.year = 'numeric';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Converts a given UTC date into the equivalent local-timestamp one.
3
+ */
4
+ export function localDateFromUTC(date: Date): Date {
5
+ return new Date(date.getTime() - date.getTimezoneOffset() * 1000 * 60);
6
+ }
7
+
8
+ /**
9
+ * Returns whether a given UTC date corresponds to the first
10
+ * millisecond of the year (e.g., Jan 1 at exactly midnight).
11
+ */
12
+ export function isFirstMillisecondOfUTCYear(date?: Date): boolean {
13
+ if (!date) return false;
14
+ return localDateFromUTC(date).toISOString().endsWith('-01-01T00:00:00.000Z');
15
+ }
@@ -3,7 +3,7 @@ import { aTimeout, expect, fixture } from '@open-wc/testing';
3
3
  import { html } from 'lit';
4
4
  import sinon from 'sinon';
5
5
  import type { InfiniteScroller } from '@internetarchive/infinite-scroller';
6
- import { SearchType } from '@internetarchive/search-service';
6
+ import { FilterConstraint, SearchType } from '@internetarchive/search-service';
7
7
  import type { HistogramDateRange } from '@internetarchive/histogram-date-range';
8
8
  import type { CollectionBrowser } from '../src/collection-browser';
9
9
  import '../src/collection-browser';
@@ -174,12 +174,20 @@ describe('Collection Browser', () => {
174
174
  el.selectedFacets = mockedSelectedFacets;
175
175
  await el.updateComplete;
176
176
 
177
- el.facetClickHandler('mediatype', true, false);
177
+ el.facetClickHandler(
178
+ new CustomEvent('facetClick', {
179
+ detail: { key: 'mediatype', state: 'selected', negative: false },
180
+ })
181
+ );
178
182
  expect(mockAnalyticsHandler.callCategory).to.equal('search-service');
179
183
  expect(mockAnalyticsHandler.callAction).to.equal('facetSelected');
180
184
  expect(mockAnalyticsHandler.callLabel).to.equal('mediatype');
181
185
 
182
- el.facetClickHandler('mediatype', false, false);
186
+ el.facetClickHandler(
187
+ new CustomEvent('facetClick', {
188
+ detail: { key: 'mediatype', state: 'none', negative: false },
189
+ })
190
+ );
183
191
  expect(el.selectedFacets).to.equal(mockedSelectedFacets);
184
192
  expect(mockAnalyticsHandler.callCategory).to.equal('search-service');
185
193
  expect(mockAnalyticsHandler.callAction).to.equal('facetDeselected');
@@ -208,12 +216,20 @@ describe('Collection Browser', () => {
208
216
  el.selectedFacets = mockedSelectedFacets;
209
217
  await el.updateComplete;
210
218
 
211
- el.facetClickHandler('mediatype', true, true);
219
+ el.facetClickHandler(
220
+ new CustomEvent('facetClick', {
221
+ detail: { key: 'mediatype', state: 'hidden', negative: true },
222
+ })
223
+ );
212
224
  expect(mockAnalyticsHandler.callCategory).to.equal('beta-search-service');
213
225
  expect(mockAnalyticsHandler.callAction).to.equal('facetNegativeSelected');
214
226
  expect(mockAnalyticsHandler.callLabel).to.equal('mediatype');
215
227
 
216
- el.facetClickHandler('mediatype', false, true);
228
+ el.facetClickHandler(
229
+ new CustomEvent('facetClick', {
230
+ detail: { key: 'mediatype', state: 'none', negative: true },
231
+ })
232
+ );
217
233
  expect(el.selectedFacets).to.equal(mockedSelectedFacets);
218
234
  expect(mockAnalyticsHandler.callCategory).to.equal('beta-search-service');
219
235
  expect(mockAnalyticsHandler.callAction).to.equal('facetNegativeDeselected');
@@ -576,6 +592,50 @@ describe('Collection Browser', () => {
576
592
  ]);
577
593
  });
578
594
 
595
+ it('queries for collection names after an aggregations fetch', async () => {
596
+ const searchService = new MockSearchService();
597
+ const collectionNameCache = new MockCollectionNameCache();
598
+
599
+ const el = await fixture<CollectionBrowser>(
600
+ html`<collection-browser
601
+ .searchService=${searchService}
602
+ .collectionNameCache=${collectionNameCache}
603
+ >
604
+ </collection-browser>`
605
+ );
606
+
607
+ el.baseQuery = 'collection-aggregations';
608
+ await el.updateComplete;
609
+
610
+ expect(collectionNameCache.preloadIdentifiersRequested).to.deep.equal([
611
+ 'foo',
612
+ 'bar',
613
+ ]);
614
+ });
615
+
616
+ it('adds collection names to cache when present on response', async () => {
617
+ const searchService = new MockSearchService();
618
+ const collectionNameCache = new MockCollectionNameCache();
619
+
620
+ const el = await fixture<CollectionBrowser>(
621
+ html`<collection-browser
622
+ .searchService=${searchService}
623
+ .collectionNameCache=${collectionNameCache}
624
+ >
625
+ </collection-browser>`
626
+ );
627
+
628
+ el.baseQuery = 'collection-titles';
629
+ await el.updateComplete;
630
+
631
+ expect(collectionNameCache.knownTitlesAdded).to.deep.equal({
632
+ foo: 'Foo Collection',
633
+ bar: 'Bar Collection',
634
+ baz: 'Baz Collection',
635
+ boop: 'Boop Collection',
636
+ });
637
+ });
638
+
579
639
  it('keeps search results from fetch if no change to query or sort param', async () => {
580
640
  const resultsSpy = sinon.spy();
581
641
  const searchService = new MockSearchService({
@@ -727,8 +787,9 @@ describe('Collection Browser', () => {
727
787
  el.selectedTitleFilter = 'X';
728
788
  await el.updateComplete;
729
789
 
730
- expect(searchService.searchParams?.query).to.equal(
731
- 'first-title AND firstTitle:X'
790
+ expect(searchService.searchParams?.query).to.equal('first-title');
791
+ expect(searchService.searchParams?.filters?.firstTitle?.X).to.equal(
792
+ FilterConstraint.INCLUDE
732
793
  );
733
794
  });
734
795
 
@@ -745,8 +806,9 @@ describe('Collection Browser', () => {
745
806
  el.selectedCreatorFilter = 'X';
746
807
  await el.updateComplete;
747
808
 
748
- expect(searchService.searchParams?.query).to.equal(
749
- 'first-creator AND firstCreator:X'
809
+ expect(searchService.searchParams?.query).to.equal('first-creator');
810
+ expect(searchService.searchParams?.filters?.firstCreator?.X).to.equal(
811
+ FilterConstraint.INCLUDE
750
812
  );
751
813
  });
752
814
 
@@ -776,9 +838,7 @@ describe('Collection Browser', () => {
776
838
  el.selectedCreatorFilter = 'X';
777
839
  await el.updateComplete;
778
840
 
779
- expect(searchService.searchParams?.query).to.equal(
780
- 'first-creator AND firstCreator:X'
781
- );
841
+ expect(searchService.searchParams?.query).to.equal('first-creator');
782
842
  expect(searchService.searchParams?.filters).to.deep.equal({
783
843
  collection: {
784
844
  foo: 'inc',
@@ -787,9 +847,38 @@ describe('Collection Browser', () => {
787
847
  '1950': 'gte',
788
848
  '1970': 'lte',
789
849
  },
850
+ firstCreator: {
851
+ X: 'inc',
852
+ },
790
853
  });
791
854
  });
792
855
 
856
+ it('resets letter filters when query changes', async () => {
857
+ const searchService = new MockSearchService();
858
+ const el = await fixture<CollectionBrowser>(
859
+ html`<collection-browser .searchService=${searchService}>
860
+ </collection-browser>`
861
+ );
862
+
863
+ el.baseQuery = 'first-creator';
864
+ el.selectedSort = 'creator' as SortField;
865
+ el.sortDirection = 'asc';
866
+ el.selectedCreatorFilter = 'X';
867
+ await el.updateComplete;
868
+ await nextTick();
869
+
870
+ expect(searchService.searchParams?.query).to.equal('first-creator');
871
+ expect(searchService.searchParams?.filters?.firstCreator?.X).to.equal(
872
+ FilterConstraint.INCLUDE
873
+ );
874
+
875
+ el.baseQuery = 'collection:foo';
876
+ await el.updateComplete;
877
+
878
+ expect(searchService.searchParams?.query).to.equal('collection:foo');
879
+ expect(searchService.searchParams?.filters?.firstCreator).not.to.exist;
880
+ });
881
+
793
882
  it('sets date range query when date picker selection changed', async () => {
794
883
  const searchService = new MockSearchService();
795
884
  const el = await fixture<CollectionBrowser>(
@@ -1,7 +1,9 @@
1
1
  import { expect, fixture } from '@open-wc/testing';
2
+ import sinon from 'sinon';
2
3
  import { html } from 'lit';
3
4
  import type { FacetsTemplate } from '../../src/collection-facets/facets-template';
4
5
  import '../../src/collection-facets/facets-template';
6
+ import { getDefaultSelectedFacets, FacetEventDetails } from '../../src/models';
5
7
 
6
8
  const facetGroup = {
7
9
  title: 'Media Type',
@@ -102,4 +104,100 @@ describe('Render facets', () => {
102
104
  'Hide mediatype: movies'
103
105
  );
104
106
  });
107
+
108
+ it('emits facetClick events for normal facets', async () => {
109
+ const facetClickSpy = sinon.spy();
110
+ const mediatypeGroup = {
111
+ title: 'Media Type',
112
+ key: 'mediatype',
113
+ buckets: [
114
+ { displayText: 'audio', key: 'audio', count: 42, state: 'none' },
115
+ ],
116
+ };
117
+ const selectedFacets = getDefaultSelectedFacets();
118
+ const el = await fixture<FacetsTemplate>(
119
+ html`<facets-template
120
+ .facetGroup=${mediatypeGroup}
121
+ .selectedFacets=${selectedFacets}
122
+ @facetClick=${facetClickSpy}
123
+ ></facets-template>`
124
+ );
125
+
126
+ const checkbox = el.shadowRoot?.querySelector(
127
+ '.select-facet-checkbox'
128
+ ) as HTMLInputElement;
129
+ expect(checkbox).to.exist;
130
+
131
+ // Select it
132
+ checkbox.click();
133
+ await el.updateComplete;
134
+ expect(facetClickSpy.callCount).to.equal(1);
135
+
136
+ const selectEvent = facetClickSpy
137
+ .args[0][0] as CustomEvent<FacetEventDetails>;
138
+ expect(selectEvent).to.exist;
139
+ expect(selectEvent?.detail?.key).to.equal('mediatype');
140
+ expect(selectEvent?.detail?.state).to.equal('selected');
141
+ expect(selectEvent?.detail?.negative).to.be.false;
142
+
143
+ // Unselect it
144
+ checkbox.click();
145
+ await el.updateComplete;
146
+ expect(facetClickSpy.callCount).to.equal(2);
147
+
148
+ const unselectEvent = facetClickSpy
149
+ .args[1][0] as CustomEvent<FacetEventDetails>;
150
+ expect(unselectEvent).to.exist;
151
+ expect(unselectEvent?.detail?.key).to.equal('mediatype');
152
+ expect(unselectEvent?.detail?.state).to.equal('none');
153
+ expect(unselectEvent?.detail?.negative).to.be.false;
154
+ });
155
+
156
+ it('emits facetClick events for negative facets', async () => {
157
+ const facetClickSpy = sinon.spy();
158
+ const mediatypeGroup = {
159
+ title: 'Media Type',
160
+ key: 'mediatype',
161
+ buckets: [
162
+ { displayText: 'audio', key: 'audio', count: 42, state: 'none' },
163
+ ],
164
+ };
165
+ const selectedFacets = getDefaultSelectedFacets();
166
+ const el = await fixture<FacetsTemplate>(
167
+ html`<facets-template
168
+ .facetGroup=${mediatypeGroup}
169
+ .selectedFacets=${selectedFacets}
170
+ @facetClick=${facetClickSpy}
171
+ ></facets-template>`
172
+ );
173
+
174
+ const checkbox = el.shadowRoot?.querySelector(
175
+ '.hide-facet-checkbox'
176
+ ) as HTMLInputElement;
177
+ expect(checkbox).to.exist;
178
+
179
+ // Select it
180
+ checkbox.click();
181
+ await el.updateComplete;
182
+ expect(facetClickSpy.callCount).to.equal(1);
183
+
184
+ const selectEvent = facetClickSpy
185
+ .args[0][0] as CustomEvent<FacetEventDetails>;
186
+ expect(selectEvent).to.exist;
187
+ expect(selectEvent?.detail?.key).to.equal('mediatype');
188
+ expect(selectEvent?.detail?.state).to.equal('hidden');
189
+ expect(selectEvent?.detail?.negative).to.be.true;
190
+
191
+ // Unselect it
192
+ checkbox.click();
193
+ await el.updateComplete;
194
+ expect(facetClickSpy.callCount).to.equal(2);
195
+
196
+ const unselectEvent = facetClickSpy
197
+ .args[1][0] as CustomEvent<FacetEventDetails>;
198
+ expect(unselectEvent).to.exist;
199
+ expect(unselectEvent?.detail?.key).to.equal('mediatype');
200
+ expect(unselectEvent?.detail?.state).to.equal('none');
201
+ expect(unselectEvent?.detail?.negative).to.be.true;
202
+ });
105
203
  });
@@ -5,6 +5,8 @@ export class MockCollectionNameCache implements CollectionNameCacheInterface {
5
5
 
6
6
  preloadIdentifiersRequested: string[] = [];
7
7
 
8
+ knownTitlesAdded: Record<string, string> = {};
9
+
8
10
  async collectionNameFor(identifier: string): Promise<string | null> {
9
11
  this.collectionNamesRequested.push(identifier);
10
12
  return `${identifier}-name`;
@@ -13,4 +15,10 @@ export class MockCollectionNameCache implements CollectionNameCacheInterface {
13
15
  async preloadIdentifiers(identifiers: string[]): Promise<void> {
14
16
  this.preloadIdentifiersRequested = identifiers;
15
17
  }
18
+
19
+ async addKnownTitles(
20
+ identifierTitleMap: Record<string, string>
21
+ ): Promise<void> {
22
+ this.knownTitlesAdded = identifierTitleMap;
23
+ }
16
24
  }
@@ -272,6 +272,95 @@ export const getMockSuccessFirstCreatorResult: () => Result<
272
272
  },
273
273
  });
274
274
 
275
+ export const getMockSuccessWithCollectionTitles: () => Result<
276
+ SearchResponse,
277
+ SearchServiceError
278
+ > = () => ({
279
+ success: {
280
+ request: {
281
+ clientParameters: {
282
+ user_query: 'collection:foo',
283
+ sort: [],
284
+ },
285
+ finalizedParameters: {
286
+ user_query: 'collection:foo',
287
+ sort: [],
288
+ },
289
+ },
290
+ rawResponse: {},
291
+ response: {
292
+ totalResults: 2,
293
+ returnedCount: 2,
294
+ results: [
295
+ new ItemHit({
296
+ fields: {
297
+ identifier: 'foo',
298
+ collection: ['foo', 'bar'],
299
+ },
300
+ }),
301
+ new ItemHit({
302
+ fields: {
303
+ identifier: 'bar',
304
+ collection: ['baz', 'boop'],
305
+ },
306
+ }),
307
+ ],
308
+ collectionTitles: {
309
+ foo: 'Foo Collection',
310
+ bar: 'Bar Collection',
311
+ baz: 'Baz Collection',
312
+ boop: 'Boop Collection',
313
+ },
314
+ },
315
+ responseHeader: {
316
+ succeeded: true,
317
+ query_time: 0,
318
+ },
319
+ },
320
+ });
321
+
322
+ export const getMockSuccessWithCollectionAggregations: () => Result<
323
+ SearchResponse,
324
+ SearchServiceError
325
+ > = () => ({
326
+ success: {
327
+ request: {
328
+ clientParameters: {
329
+ user_query: 'collection:foo',
330
+ sort: [],
331
+ },
332
+ finalizedParameters: {
333
+ user_query: 'collection:foo',
334
+ sort: [],
335
+ },
336
+ },
337
+ rawResponse: {},
338
+ response: {
339
+ totalResults: 0,
340
+ returnedCount: 0,
341
+ results: [],
342
+ aggregations: {
343
+ collection: new Aggregation({
344
+ buckets: [
345
+ {
346
+ key: 'foo',
347
+ doc_count: 10,
348
+ },
349
+ {
350
+ key: 'bar',
351
+ doc_count: 10,
352
+ },
353
+ ],
354
+ }),
355
+ },
356
+ },
357
+ responseHeader: {
358
+ succeeded: true,
359
+ query_time: 0,
360
+ },
361
+ },
362
+ });
363
+
275
364
  export const getMockSuccessSingleResultWithSort: (
276
365
  resultsSpy: Function
277
366
  ) => Result<SearchResponse, SearchServiceError> = (resultsSpy: Function) => ({