@internetarchive/collection-browser 0.3.7-alpha.1 → 0.3.8

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 (31) hide show
  1. package/dist/src/app-root.d.ts +1 -0
  2. package/dist/src/app-root.js +29 -8
  3. package/dist/src/app-root.js.map +1 -1
  4. package/dist/src/collection-browser.d.ts +33 -1
  5. package/dist/src/collection-browser.js +118 -44
  6. package/dist/src/collection-browser.js.map +1 -1
  7. package/dist/src/collection-facets/more-facets-content.js +3 -3
  8. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  9. package/dist/src/collection-facets.js +1 -1
  10. package/dist/src/collection-facets.js.map +1 -1
  11. package/dist/src/tiles/list/tile-list.js +6 -3
  12. package/dist/src/tiles/list/tile-list.js.map +1 -1
  13. package/dist/test/collection-browser.test.js +139 -0
  14. package/dist/test/collection-browser.test.js.map +1 -1
  15. package/dist/test/mocks/mock-search-responses.d.ts +2 -0
  16. package/dist/test/mocks/mock-search-responses.js +74 -1
  17. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  18. package/dist/test/mocks/mock-search-service.js +5 -1
  19. package/dist/test/mocks/mock-search-service.js.map +1 -1
  20. package/dist/test/tiles/list/tile-list.test.js +29 -0
  21. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/app-root.ts +33 -10
  24. package/src/collection-browser.ts +139 -44
  25. package/src/collection-facets/more-facets-content.ts +3 -3
  26. package/src/collection-facets.ts +1 -1
  27. package/src/tiles/list/tile-list.ts +6 -2
  28. package/test/collection-browser.test.ts +207 -0
  29. package/test/mocks/mock-search-responses.ts +82 -0
  30. package/test/mocks/mock-search-service.ts +6 -0
  31. package/test/tiles/list/tile-list.test.ts +40 -0
@@ -4,6 +4,7 @@ import { html } from 'lit';
4
4
  import sinon from 'sinon';
5
5
  import type { InfiniteScroller } from '@internetarchive/infinite-scroller';
6
6
  import { SearchType } from '@internetarchive/search-service';
7
+ import type { HistogramDateRange } from '@internetarchive/histogram-date-range';
7
8
  import type { CollectionBrowser } from '../src/collection-browser';
8
9
  import '../src/collection-browser';
9
10
  import {
@@ -17,6 +18,7 @@ import { MockCollectionNameCache } from './mocks/mock-collection-name-cache';
17
18
  import { MockAnalyticsHandler } from './mocks/mock-analytics-handler';
18
19
  import { analyticsCategories } from '../src/utils/analytics-events';
19
20
  import type { TileDispatcher } from '../src/tiles/tile-dispatcher';
21
+ import type { CollectionFacets } from '../src/collection-facets';
20
22
 
21
23
  describe('Collection Browser', () => {
22
24
  beforeEach(async () => {
@@ -289,6 +291,98 @@ describe('Collection Browser', () => {
289
291
  );
290
292
  });
291
293
 
294
+ it('fires a separate date histogram query when year facets are applied', async () => {
295
+ const searchService = new MockSearchService();
296
+ const selectedFacets: SelectedFacets = {
297
+ subject: {},
298
+ lending: {},
299
+ mediatype: {},
300
+ language: {},
301
+ creator: {},
302
+ collection: {},
303
+ year: {
304
+ '2000': {
305
+ key: '2000',
306
+ state: 'selected',
307
+ count: 123,
308
+ },
309
+ },
310
+ };
311
+
312
+ const el = await fixture<CollectionBrowser>(
313
+ html`<collection-browser .searchService=${searchService}>
314
+ </collection-browser>`
315
+ );
316
+
317
+ el.baseQuery = 'collection:foo';
318
+ el.showHistogramDatePicker = true;
319
+ el.selectedFacets = selectedFacets;
320
+ await el.updateComplete;
321
+
322
+ expect(
323
+ searchService.searchParams?.aggregations?.simpleParams
324
+ ).to.deep.equal(['year']); // Explicitly requests year aggregations
325
+ expect(searchService.searchParams?.query).to.equal(
326
+ 'collection:foo' // No date clause on query
327
+ );
328
+ });
329
+
330
+ it('fires a separate date histogram query when a date range is applied', async () => {
331
+ const searchService = new MockSearchService();
332
+
333
+ const el = await fixture<CollectionBrowser>(
334
+ html`<collection-browser .searchService=${searchService}>
335
+ </collection-browser>`
336
+ );
337
+
338
+ el.baseQuery = 'collection:foo';
339
+ el.showHistogramDatePicker = true;
340
+ el.dateRangeQueryClause = 'year:[1995 TO 2005]';
341
+ await el.updateComplete;
342
+
343
+ expect(
344
+ searchService.searchParams?.aggregations?.simpleParams
345
+ ).to.deep.equal(['year']); // Explicitly requests year aggregations
346
+ expect(searchService.searchParams?.query).to.equal(
347
+ 'collection:foo' // No date clause on query
348
+ );
349
+ });
350
+
351
+ it('does not fire a separate date histogram query when no date filters are applied', async () => {
352
+ const searchService = new MockSearchService();
353
+
354
+ const el = await fixture<CollectionBrowser>(
355
+ html`<collection-browser .searchService=${searchService}>
356
+ </collection-browser>`
357
+ );
358
+
359
+ el.baseQuery = 'collection:foo';
360
+ el.showHistogramDatePicker = true;
361
+ await el.updateComplete;
362
+
363
+ expect(searchService.searchParams?.aggregations?.simpleParams).to.satisfy(
364
+ (aggTypes: string[]) => !aggTypes || !aggTypes.includes('year') // Not requesting year aggregations explicitly
365
+ );
366
+ });
367
+
368
+ it('does not fire a separate date histogram query when date picker is disabled', async () => {
369
+ const searchService = new MockSearchService();
370
+
371
+ const el = await fixture<CollectionBrowser>(
372
+ html`<collection-browser .searchService=${searchService}>
373
+ </collection-browser>`
374
+ );
375
+
376
+ el.baseQuery = 'collection:foo';
377
+ el.showHistogramDatePicker = false;
378
+ el.dateRangeQueryClause = 'year:[1995 TO 2005]';
379
+ await el.updateComplete;
380
+
381
+ expect(searchService.searchParams?.aggregations?.simpleParams).to.satisfy(
382
+ (aggTypes: string[]) => !aggTypes || !aggTypes.includes('year') // Not requesting year aggregations explicitly
383
+ );
384
+ });
385
+
292
386
  it('fails gracefully if no search service provided', async () => {
293
387
  const el = await fixture<CollectionBrowser>(
294
388
  html`<collection-browser></collection-browser>`
@@ -381,6 +475,28 @@ describe('Collection Browser', () => {
381
475
  expect(cell.model?.contentWarning).to.be.true;
382
476
  });
383
477
 
478
+ it('joins full description array into a single string with line breaks', async () => {
479
+ const searchService = new MockSearchService();
480
+
481
+ const el = await fixture<CollectionBrowser>(
482
+ html`<collection-browser .searchService=${searchService}>
483
+ </collection-browser>`
484
+ );
485
+
486
+ // This query receives an array description like ['line1', 'line2']
487
+ el.baseQuery = 'multi-line-description';
488
+ await el.updateComplete;
489
+
490
+ const cellTemplate = el.cellForIndex(0);
491
+ expect(cellTemplate).to.exist;
492
+
493
+ const cell = await fixture<TileDispatcher>(cellTemplate!);
494
+ expect(cell).to.exist;
495
+
496
+ // Actual model description should be joined
497
+ expect(cell.model?.description).to.equal('line1\nline2');
498
+ });
499
+
384
500
  it('can search on demand if only search type has changed', async () => {
385
501
  const searchService = new MockSearchService();
386
502
 
@@ -552,6 +668,97 @@ describe('Collection Browser', () => {
552
668
  expect(el.selectedSort).to.equal(SortField.title);
553
669
  });
554
670
 
671
+ it('sets sort filter properties when user selects title filter', async () => {
672
+ const searchService = new MockSearchService();
673
+ const el = await fixture<CollectionBrowser>(
674
+ html`<collection-browser .searchService=${searchService}>
675
+ </collection-browser>`
676
+ );
677
+
678
+ el.baseQuery = 'collection:foo';
679
+ el.selectedTitleFilter = 'X';
680
+ await el.updateComplete;
681
+
682
+ // Wait an extra tick
683
+ await new Promise(res => {
684
+ setTimeout(res, 0);
685
+ });
686
+
687
+ expect(searchService.searchParams?.query).to.equal(
688
+ 'collection:foo AND firstTitle:X'
689
+ );
690
+ });
691
+
692
+ it('sets sort filter properties when user selects creator filter', async () => {
693
+ const searchService = new MockSearchService();
694
+ const el = await fixture<CollectionBrowser>(
695
+ html`<collection-browser .searchService=${searchService}>
696
+ </collection-browser>`
697
+ );
698
+
699
+ el.baseQuery = 'collection:foo';
700
+ el.selectedCreatorFilter = 'X';
701
+ await el.updateComplete;
702
+
703
+ // Wait an extra tick
704
+ await new Promise(res => {
705
+ setTimeout(res, 0);
706
+ });
707
+
708
+ expect(searchService.searchParams?.query).to.equal(
709
+ 'collection:foo AND firstCreator:X'
710
+ );
711
+ });
712
+
713
+ it('sets date range query when date picker selection changed', async () => {
714
+ const searchService = new MockSearchService();
715
+ const el = await fixture<CollectionBrowser>(
716
+ html`<collection-browser .searchService=${searchService}>
717
+ </collection-browser>`
718
+ );
719
+
720
+ el.baseQuery = 'years'; // Includes year_histogram aggregation in response
721
+ el.showHistogramDatePicker = true;
722
+ await el.updateComplete;
723
+
724
+ const facets = el.shadowRoot?.querySelector(
725
+ 'collection-facets'
726
+ ) as CollectionFacets;
727
+ await facets?.updateComplete;
728
+
729
+ // Wait for the date picker to be rendered (which may take until the next tick)
730
+ await new Promise(res => {
731
+ setTimeout(res, 0);
732
+ });
733
+
734
+ const histogram = facets?.shadowRoot?.querySelector(
735
+ 'histogram-date-range'
736
+ ) as HistogramDateRange;
737
+ expect(histogram).to.exist;
738
+
739
+ // Enter a new min date into the date picker
740
+ const minDateInput = histogram.shadowRoot?.querySelector(
741
+ '#date-min'
742
+ ) as HTMLInputElement;
743
+
744
+ const pressEnterEvent = new KeyboardEvent('keyup', {
745
+ key: 'Enter',
746
+ });
747
+
748
+ minDateInput.value = '1960';
749
+ minDateInput.dispatchEvent(pressEnterEvent);
750
+
751
+ // Wait for the histogram's update delay
752
+ await new Promise(res => {
753
+ setTimeout(res, histogram.updateDelay + 50);
754
+ });
755
+
756
+ // Ensure that the histogram change propagated to the collection browser's
757
+ // date query correctly.
758
+ await el.updateComplete;
759
+ expect(el.dateRangeQueryClause).to.equal('year:[1960 TO 2000]');
760
+ });
761
+
555
762
  it('scrolls to page', async () => {
556
763
  const searchService = new MockSearchService();
557
764
  const el = await fixture<CollectionBrowser>(
@@ -1,5 +1,6 @@
1
1
  import type { Result } from '@internetarchive/result-type';
2
2
  import {
3
+ Aggregation,
3
4
  ItemHit,
4
5
  SearchResponse,
5
6
  SearchServiceError,
@@ -40,6 +41,51 @@ export const mockSuccessSingleResult: Result<
40
41
  },
41
42
  };
42
43
 
44
+ export const mockSuccessWithYearHistogramAggs: Result<
45
+ SearchResponse,
46
+ SearchServiceError
47
+ > = {
48
+ success: {
49
+ request: {
50
+ clientParameters: {
51
+ user_query: 'years',
52
+ sort: [],
53
+ },
54
+ finalizedParameters: {
55
+ user_query: 'years',
56
+ sort: [],
57
+ },
58
+ },
59
+ rawResponse: {},
60
+ response: {
61
+ totalResults: 1,
62
+ returnedCount: 1,
63
+ aggregations: {
64
+ subject: new Aggregation({
65
+ buckets: [
66
+ {
67
+ key: 'foo',
68
+ doc_count: 3,
69
+ },
70
+ ],
71
+ }),
72
+ year_histogram: new Aggregation({
73
+ buckets: [1, 2, 3, 3, 2, 1],
74
+ first_bucket_key: 1950,
75
+ last_bucket_key: 2000,
76
+ interval: 10,
77
+ number_buckets: 6,
78
+ }),
79
+ },
80
+ results: [],
81
+ },
82
+ responseHeader: {
83
+ succeeded: true,
84
+ query_time: 0,
85
+ },
86
+ },
87
+ };
88
+
43
89
  export const mockSuccessLoggedInResult: Result<
44
90
  SearchResponse,
45
91
  SearchServiceError
@@ -224,3 +270,39 @@ export const mockSuccessMultipleResults: Result<
224
270
  },
225
271
  },
226
272
  };
273
+
274
+ export const mockSuccessMultiLineDescription: Result<
275
+ SearchResponse,
276
+ SearchServiceError
277
+ > = {
278
+ success: {
279
+ request: {
280
+ clientParameters: {
281
+ user_query: 'multi-line-description',
282
+ sort: [],
283
+ },
284
+ finalizedParameters: {
285
+ user_query: 'multi-line-description',
286
+ sort: [],
287
+ },
288
+ },
289
+ rawResponse: {},
290
+ response: {
291
+ totalResults: 1,
292
+ returnedCount: 1,
293
+ results: [
294
+ new ItemHit({
295
+ fields: {
296
+ identifier: 'foo',
297
+ collection: ['foo', 'bar'],
298
+ description: ['line1', 'line2'],
299
+ },
300
+ }),
301
+ ],
302
+ },
303
+ responseHeader: {
304
+ succeeded: true,
305
+ query_time: 0,
306
+ },
307
+ },
308
+ };
@@ -13,6 +13,8 @@ import {
13
13
  mockSuccessLoggedInResult,
14
14
  mockSuccessNoPreviewResult,
15
15
  mockSuccessLoggedInAndNoPreviewResult,
16
+ mockSuccessWithYearHistogramAggs,
17
+ mockSuccessMultiLineDescription,
16
18
  } from './mock-search-responses';
17
19
 
18
20
  export class MockSearchService implements SearchServiceInterface {
@@ -46,6 +48,10 @@ export class MockSearchService implements SearchServiceInterface {
46
48
  switch (this.searchParams?.query) {
47
49
  case 'single-result':
48
50
  return mockSuccessSingleResult;
51
+ case 'years':
52
+ return mockSuccessWithYearHistogramAggs;
53
+ case 'multi-line-description':
54
+ return mockSuccessMultiLineDescription;
49
55
  case 'loggedin':
50
56
  return mockSuccessLoggedInResult;
51
57
  case 'no-preview':
@@ -105,4 +105,44 @@ describe('List Tile', () => {
105
105
  expect(dateRow).to.exist;
106
106
  expect(dateRow?.textContent?.trim()).to.contain('Added: Jan 01, 2010');
107
107
  });
108
+
109
+ it('should render links to /search pages (not search.php) for subject, creator, and source', async () => {
110
+ const model: Partial<TileModel> = {
111
+ subjects: ['foo'],
112
+ creators: ['bar'],
113
+ source: 'baz',
114
+ };
115
+
116
+ const el = await fixture<TileList>(html`
117
+ <tile-list .model=${model}></tile-list>
118
+ `);
119
+
120
+ const subjectLink = el.shadowRoot?.querySelector('#topics a[href]');
121
+ expect(subjectLink).to.exist;
122
+ expect(subjectLink?.getAttribute('href')).to.equal(
123
+ `/search?query=${encodeURIComponent('subject:"foo"')}`
124
+ );
125
+
126
+ const creatorLink = el.shadowRoot?.querySelector('#creator a[href]');
127
+ expect(creatorLink).to.exist;
128
+ expect(creatorLink?.getAttribute('href')).to.equal(
129
+ `/search?query=${encodeURIComponent('creator:"bar"')}`
130
+ );
131
+
132
+ const sourceLink = el.shadowRoot?.querySelector('#source a[href]');
133
+ expect(sourceLink).to.exist;
134
+ expect(sourceLink?.getAttribute('href')).to.equal(
135
+ `/search?query=${encodeURIComponent('source:"baz"')}`
136
+ );
137
+ });
138
+
139
+ it('should render multi-line descriptions with spaces b/w lines', async () => {
140
+ const el = await fixture<TileList>(html`
141
+ <tile-list .model=${{ description: 'line1\nline2' }}> </tile-list>
142
+ `);
143
+
144
+ const descriptionBlock = el.shadowRoot?.getElementById('description');
145
+ expect(descriptionBlock).to.exist;
146
+ expect(descriptionBlock?.textContent?.trim()).to.equal('line1 line2'); // line break replaced by space
147
+ });
108
148
  });