@internetarchive/collection-browser 1.4.1-alpha.6 → 1.4.1

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 (46) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/app-root.d.ts +0 -3
  5. package/dist/src/app-root.js +1 -27
  6. package/dist/src/app-root.js.map +1 -1
  7. package/dist/src/collection-browser.d.ts +11 -8
  8. package/dist/src/collection-browser.js +28 -39
  9. package/dist/src/collection-browser.js.map +1 -1
  10. package/dist/src/collection-facets/facets-template.d.ts +0 -1
  11. package/dist/src/collection-facets/facets-template.js +1 -8
  12. package/dist/src/collection-facets/facets-template.js.map +1 -1
  13. package/dist/src/collection-facets.d.ts +0 -1
  14. package/dist/src/collection-facets.js +0 -5
  15. package/dist/src/collection-facets.js.map +1 -1
  16. package/dist/src/sort-filter-bar/alpha-bar.js +1 -1
  17. package/dist/src/sort-filter-bar/alpha-bar.js.map +1 -1
  18. package/dist/src/tiles/list/tile-list-compact.d.ts +0 -1
  19. package/dist/src/tiles/list/tile-list-compact.js +4 -10
  20. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  21. package/dist/src/tiles/list/tile-list.d.ts +0 -1
  22. package/dist/src/tiles/list/tile-list.js +6 -15
  23. package/dist/src/tiles/list/tile-list.js.map +1 -1
  24. package/dist/src/tiles/tile-dispatcher.d.ts +0 -1
  25. package/dist/src/tiles/tile-dispatcher.js +4 -12
  26. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  27. package/dist/test/collection-browser.test.js +19 -1
  28. package/dist/test/collection-browser.test.js.map +1 -1
  29. package/dist/test/mocks/mock-search-responses.d.ts +1 -0
  30. package/dist/test/mocks/mock-search-responses.js +33 -1
  31. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  32. package/dist/test/mocks/mock-search-service.js +2 -1
  33. package/dist/test/mocks/mock-search-service.js.map +1 -1
  34. package/index.ts +1 -1
  35. package/package.json +1 -1
  36. package/src/app-root.ts +0 -26
  37. package/src/collection-browser.ts +28 -40
  38. package/src/collection-facets/facets-template.ts +1 -3
  39. package/src/collection-facets.ts +0 -3
  40. package/src/sort-filter-bar/alpha-bar.ts +1 -1
  41. package/src/tiles/list/tile-list-compact.ts +3 -10
  42. package/src/tiles/list/tile-list.ts +8 -21
  43. package/src/tiles/tile-dispatcher.ts +3 -12
  44. package/test/collection-browser.test.ts +28 -1
  45. package/test/mocks/mock-search-responses.ts +37 -0
  46. package/test/mocks/mock-search-service.ts +2 -0
package/src/app-root.ts CHANGED
@@ -39,8 +39,6 @@ export class AppRoot extends LitElement {
39
39
 
40
40
  @state() private searchQuery?: string;
41
41
 
42
- @state() private withinCollection?: string;
43
-
44
42
  @state() private cellWidth: number = 18;
45
43
 
46
44
  @state() private cellHeight: number = 29;
@@ -57,9 +55,6 @@ export class AppRoot extends LitElement {
57
55
 
58
56
  @query('#base-query-field') private baseQueryField!: HTMLInputElement;
59
57
 
60
- @query('#base-collection-field')
61
- private baseCollectionField!: HTMLInputElement;
62
-
63
58
  @query('#page-number-input') private pageNumberInput!: HTMLInputElement;
64
59
 
65
60
  @query('collection-browser') private collectionBrowser!: CollectionBrowser;
@@ -100,16 +95,6 @@ export class AppRoot extends LitElement {
100
95
  }
101
96
  }
102
97
 
103
- private collectionChanged(e: Event) {
104
- e.preventDefault();
105
- this.withinCollection = this.baseCollectionField.value;
106
- this.collectionBrowser.withinCollection = this.withinCollection;
107
-
108
- if ((this.currentPage ?? 1) > 1) {
109
- this.collectionBrowser.goToPage(this.currentPage ?? 1);
110
- }
111
- }
112
-
113
98
  private changePagePressed(e: Event) {
114
99
  e.preventDefault();
115
100
  this.currentPage = this.pageNumberInput.valueAsNumber;
@@ -156,17 +141,6 @@ export class AppRoot extends LitElement {
156
141
  <input type="submit" value="Go" />
157
142
  </form>
158
143
  </div>
159
- <div>
160
- <form @submit=${this.collectionChanged}>
161
- <label for="base-collection-field"> Within collection: </label>
162
- <input
163
- type="text"
164
- id="base-collection-field"
165
- .value=${this.withinCollection ?? ''}
166
- />
167
- <input type="submit" value="Search" />
168
- </form>
169
- </div>
170
144
 
171
145
  <div id="search-types">
172
146
  Search type:
@@ -90,8 +90,6 @@ export class CollectionBrowser
90
90
 
91
91
  @property({ type: String }) searchType: SearchType = SearchType.METADATA;
92
92
 
93
- @property({ type: String }) withinCollection?: string;
94
-
95
93
  @property({ type: String }) baseQuery?: string;
96
94
 
97
95
  @property({ type: String }) displayMode?: CollectionDisplayMode;
@@ -120,8 +118,6 @@ export class CollectionBrowser
120
118
 
121
119
  @property({ type: Boolean }) showHistogramDatePicker = false;
122
120
 
123
- @property({ type: String }) collectionPagePath: string = '/details/';
124
-
125
121
  /** describes where this component is being used */
126
122
  @property({ type: String, reflect: true }) searchContext: string =
127
123
  analyticsCategories.default;
@@ -386,7 +382,7 @@ export class CollectionBrowser
386
382
 
387
383
  private setPlaceholderType() {
388
384
  this.placeholderType = null;
389
- if (!this.baseQuery?.trim() && !this.withinCollection) {
385
+ if (!this.baseQuery?.trim()) {
390
386
  this.placeholderType = 'empty-query';
391
387
  } else if (
392
388
  !this.searchResultsLoading &&
@@ -716,7 +712,6 @@ export class CollectionBrowser
716
712
  <collection-facets
717
713
  @facetsChanged=${this.facetsChanged}
718
714
  @histogramDateRangeUpdated=${this.histogramDateRangeUpdated}
719
- .collectionPagePath=${this.collectionPagePath}
720
715
  .searchService=${this.searchService}
721
716
  .featureFeedbackService=${this.featureFeedbackService}
722
717
  .recaptchaManager=${this.recaptchaManager}
@@ -914,8 +909,7 @@ export class CollectionBrowser
914
909
  changed.has('maxSelectedDate') ||
915
910
  changed.has('sortParam') ||
916
911
  changed.has('selectedFacets') ||
917
- changed.has('searchService') ||
918
- changed.has('withinCollection')
912
+ changed.has('searchService')
919
913
  ) {
920
914
  this.handleQueryChange();
921
915
  }
@@ -956,8 +950,7 @@ export class CollectionBrowser
956
950
  const previousView = this.mobileView;
957
951
  if (entry.target === this.contentContainer) {
958
952
  this.contentWidth = entry.contentRect.width;
959
- this.mobileView =
960
- this.contentWidth > 0 && this.contentWidth < this.mobileBreakpoint;
953
+ this.mobileView = this.contentWidth < this.mobileBreakpoint;
961
954
  // If changing from desktop to mobile disable transition
962
955
  if (this.mobileView && !previousView) {
963
956
  this.isResizeToMobile = true;
@@ -1354,7 +1347,8 @@ export class CollectionBrowser
1354
1347
 
1355
1348
  /** The full query, including year facets and date range clauses */
1356
1349
  private get fullQuery(): string | undefined {
1357
- let fullQuery = this.baseQuery?.trim() ?? '';
1350
+ if (!this.baseQuery) return undefined;
1351
+ let fullQuery = this.baseQuery.trim();
1358
1352
 
1359
1353
  const { facetQuery, dateRangeQueryClause, sortFilterQueries } = this;
1360
1354
 
@@ -1486,15 +1480,14 @@ export class CollectionBrowser
1486
1480
 
1487
1481
  private async fetchFacets() {
1488
1482
  const trimmedQuery = this.baseQuery?.trim();
1489
- if (!trimmedQuery && !this.withinCollection) return;
1483
+ if (!trimmedQuery) return;
1490
1484
  if (!this.searchService) return;
1491
1485
 
1492
1486
  const { facetFetchQueryKey } = this;
1493
1487
 
1494
1488
  const sortParams = this.sortParam ? [this.sortParam] : [];
1495
1489
  const params: SearchParams = {
1496
- ...this.collectionParams,
1497
- query: trimmedQuery || '',
1490
+ query: trimmedQuery,
1498
1491
  rows: 0,
1499
1492
  filters: this.filterMap,
1500
1493
  // Fetch a few extra buckets beyond the 6 we show, in case some get suppressed
@@ -1576,24 +1569,10 @@ export class CollectionBrowser
1576
1569
  });
1577
1570
  }
1578
1571
 
1579
- /**
1580
- * Additional params to pass to the search service if targeting a collection page,
1581
- * or null otherwise.
1582
- */
1583
- private get collectionParams(): {
1584
- pageType: string;
1585
- pageTarget: string;
1586
- } | null {
1587
- return this.withinCollection
1588
- ? { pageType: 'collection_details', pageTarget: this.withinCollection }
1589
- : null;
1590
- }
1591
-
1592
1572
  /**
1593
1573
  * The query key is a string that uniquely identifies the current search.
1594
1574
  * It consists of:
1595
1575
  * - The current base query
1596
- * - The current collection
1597
1576
  * - The current search type
1598
1577
  * - Any currently-applied facets
1599
1578
  * - Any currently-applied date range
@@ -1606,7 +1585,7 @@ export class CollectionBrowser
1606
1585
  private get pageFetchQueryKey(): string {
1607
1586
  const sortField = this.sortParam?.field ?? 'none';
1608
1587
  const sortDirection = this.sortParam?.direction ?? 'none';
1609
- return `${this.fullQuery}-${this.withinCollection}-${this.searchType}-${sortField}-${sortDirection}`;
1588
+ return `${this.fullQuery}-${this.searchType}-${sortField}-${sortDirection}`;
1610
1589
  }
1611
1590
 
1612
1591
  /**
@@ -1614,7 +1593,7 @@ export class CollectionBrowser
1614
1593
  * are not relevant in determining aggregation queries.
1615
1594
  */
1616
1595
  private get facetFetchQueryKey(): string {
1617
- return `${this.fullQuery}-${this.withinCollection}-${this.searchType}`;
1596
+ return `${this.fullQuery}-${this.searchType}`;
1618
1597
  }
1619
1598
 
1620
1599
  // this maps the query to the pages being fetched for that query
@@ -1630,7 +1609,7 @@ export class CollectionBrowser
1630
1609
  */
1631
1610
  async fetchPage(pageNumber: number, numInitialPages = 1) {
1632
1611
  const trimmedQuery = this.baseQuery?.trim();
1633
- if (!trimmedQuery && !this.withinCollection) return;
1612
+ if (!trimmedQuery) return;
1634
1613
  if (!this.searchService) return;
1635
1614
 
1636
1615
  // if we already have data, don't fetch again
@@ -1655,8 +1634,7 @@ export class CollectionBrowser
1655
1634
 
1656
1635
  const sortParams = this.sortParam ? [this.sortParam] : [];
1657
1636
  const params: SearchParams = {
1658
- ...this.collectionParams,
1659
- query: trimmedQuery || '',
1637
+ query: trimmedQuery,
1660
1638
  page: pageNumber,
1661
1639
  rows: numRows,
1662
1640
  sort: sortParams,
@@ -1808,7 +1786,7 @@ export class CollectionBrowser
1808
1786
  dateReviewed: result.reviewdate?.value,
1809
1787
  description: result.description?.values.join('\n'),
1810
1788
  favCount: result.num_favorites?.value ?? 0,
1811
- href: result.__href__?.value,
1789
+ href: this.collapseRepeatedQuotes(result.__href__?.value),
1812
1790
  identifier: result.identifier,
1813
1791
  issue: result.issue?.value,
1814
1792
  itemCount: result.item_count?.value ?? 0,
@@ -1833,19 +1811,31 @@ export class CollectionBrowser
1833
1811
  }
1834
1812
  }
1835
1813
 
1814
+ /**
1815
+ * Returns the input string, but removing one set of quotes from all instances of
1816
+ * ""clauses wrapped in two sets of quotes"". This assumes the quotes are already
1817
+ * URL-encoded.
1818
+ *
1819
+ * This should be a temporary measure to address the fact that the __href__ field
1820
+ * sometimes acquires extra quotation marks during query rewriting. Once there is a
1821
+ * full Lucene parser in place that handles quoted queries correctly, this can likely
1822
+ * be removed.
1823
+ */
1824
+ private collapseRepeatedQuotes(str?: string): string | undefined {
1825
+ return str?.replace(/%22%22(?!%22%22)(.+?)%22%22/g, '%22$1%22');
1826
+ }
1827
+
1836
1828
  /** Fetches the aggregation buckets for the given prefix filter type. */
1837
1829
  private async fetchPrefixFilterBuckets(
1838
1830
  filterType: PrefixFilterType
1839
1831
  ): Promise<Bucket[]> {
1840
1832
  const trimmedQuery = this.baseQuery?.trim();
1841
- if (!trimmedQuery && !this.withinCollection) return [];
1833
+ if (!trimmedQuery) return [];
1842
1834
 
1843
1835
  const filterAggregationKey = prefixFilterAggregationKeys[filterType];
1844
1836
  const sortParams = this.sortParam ? [this.sortParam] : [];
1845
-
1846
1837
  const params: SearchParams = {
1847
- ...this.collectionParams,
1848
- query: trimmedQuery || '',
1838
+ query: trimmedQuery,
1849
1839
  rows: 0,
1850
1840
  filters: this.filterMap,
1851
1841
  // Only fetch the firstTitle or firstCreator aggregation
@@ -1942,7 +1932,6 @@ export class CollectionBrowser
1942
1932
 
1943
1933
  return html`
1944
1934
  <tile-dispatcher
1945
- .collectionPagePath=${this.collectionPagePath}
1946
1935
  .baseNavigationUrl=${this.baseNavigationUrl}
1947
1936
  .baseImageUrl=${this.baseImageUrl}
1948
1937
  .model=${model}
@@ -2046,7 +2035,6 @@ export class CollectionBrowser
2046
2035
  border-right: 1px solid rgb(232, 232, 232);
2047
2036
  padding-left: 1rem;
2048
2037
  padding-right: 1rem;
2049
- margin-top: var(--rightColumnMarginTop, 0);
2050
2038
  background: #fff;
2051
2039
  }
2052
2040
 
@@ -22,8 +22,6 @@ export class FacetsTemplate extends LitElement {
22
22
 
23
23
  @property({ type: String }) renderOn?: string;
24
24
 
25
- @property({ type: String }) collectionPagePath: string = '/details/';
26
-
27
25
  @property({ type: Object })
28
26
  collectionNameCache?: CollectionNameCacheInterface;
29
27
 
@@ -148,7 +146,7 @@ export class FacetsTemplate extends LitElement {
148
146
  const bucketTextDisplay =
149
147
  facetGroup.key !== 'collection'
150
148
  ? html`${bucket.displayText ?? bucket.key}`
151
- : html`<a href="${this.collectionPagePath}${bucket.key}">
149
+ : html`<a href="/details/${bucket.key}">
152
150
  <async-collection-name
153
151
  .collectionNameCache=${this.collectionNameCache}
154
152
  .identifier=${bucket.key}
@@ -90,8 +90,6 @@ export class CollectionFacets extends LitElement {
90
90
 
91
91
  @property({ type: Object }) filterMap?: FilterMap;
92
92
 
93
- @property({ type: String }) collectionPagePath: string = '/details/';
94
-
95
93
  @property({ type: Object, attribute: false })
96
94
  modalManager?: ModalManagerInterface;
97
95
 
@@ -628,7 +626,6 @@ export class CollectionFacets extends LitElement {
628
626
  private getFacetTemplate(facetGroup: FacetGroup): TemplateResult {
629
627
  return html`
630
628
  <facets-template
631
- .collectionPagePath=${this.collectionPagePath}
632
629
  .facetGroup=${facetGroup}
633
630
  .selectedFacets=${this.selectedFacets}
634
631
  .renderOn=${'page'}
@@ -210,7 +210,7 @@ export class AlphaBar extends LitElement {
210
210
  border: none;
211
211
  border-radius: 4px;
212
212
  font-family: inherit;
213
- font-size: 1.2rem;
213
+ font-size: inherit;
214
214
  cursor: pointer;
215
215
  }
216
216
 
@@ -30,8 +30,6 @@ export class TileListCompact extends LitElement {
30
30
 
31
31
  @property({ type: Boolean }) loggedIn = false;
32
32
 
33
- @property({ type: String }) collectionPagePath: string = '/details/';
34
-
35
33
  render() {
36
34
  return html`
37
35
  <div id="list-line" class="${this.classSize}">
@@ -68,14 +66,9 @@ export class TileListCompact extends LitElement {
68
66
  private get href(): string {
69
67
  // Use the server-specified href if available.
70
68
  // Otherwise, construct a details page URL from the item identifier.
71
- if (this.model?.href) {
72
- return `${this.baseNavigationUrl}${this.model?.href}`;
73
- }
74
-
75
- const isCollection = this.model?.mediatype === 'collection';
76
- return `${this.baseNavigationUrl}${
77
- isCollection ? this.collectionPagePath : '/details/'
78
- }${this.model?.identifier}`;
69
+ return this.model?.href
70
+ ? `${this.baseNavigationUrl}${this.model.href}`
71
+ : `${this.baseNavigationUrl}/details/${this.model?.identifier}`;
79
72
  }
80
73
 
81
74
  /*
@@ -49,8 +49,6 @@ export class TileList extends LitElement {
49
49
 
50
50
  @property({ type: Boolean }) loggedIn = false;
51
51
 
52
- @property({ type: String }) collectionPagePath: string = '/details/';
53
-
54
52
  render() {
55
53
  return html`
56
54
  <div id="list-line" class="${this.classSize}">
@@ -95,11 +93,10 @@ export class TileList extends LitElement {
95
93
  private get imageBlockTemplate() {
96
94
  if (!this.model) return nothing;
97
95
 
98
- const isCollection = this.model.mediatype === 'collection';
99
96
  return html`<a
100
- href="${this.baseNavigationUrl}${isCollection
101
- ? this.collectionPagePath
102
- : '/details/'}${encodeURI(this.model.identifier)}"
97
+ href="${this.baseNavigationUrl}/details/${encodeURI(
98
+ this.model.identifier
99
+ )}"
103
100
  >
104
101
  <image-block
105
102
  .model=${this.model}
@@ -151,11 +148,7 @@ export class TileList extends LitElement {
151
148
  ? html`<a href="${this.baseNavigationUrl}${this.model.href}"
152
149
  >${this.model.title ?? this.model.identifier}</a
153
150
  >`
154
- : this.detailsLink(
155
- this.model.identifier,
156
- this.model.title,
157
- this.model.mediatype === 'collection'
158
- );
151
+ : this.detailsLink(this.model.identifier, this.model.title);
159
152
  }
160
153
 
161
154
  private get itemLineTemplate() {
@@ -348,18 +341,12 @@ export class TileList extends LitElement {
348
341
  /* eslint-enable lit/no-invalid-html */
349
342
  }
350
343
 
351
- private detailsLink(
352
- identifier: string,
353
- text?: string,
354
- collection = false
355
- ): TemplateResult {
344
+ private detailsLink(identifier: string, text?: string): TemplateResult {
356
345
  const linkText = text ?? identifier;
357
346
  // No whitespace after closing tag
358
347
  // identifiers (all ASCII in their creation) should be safe to use in href, but sanitize anyway
359
348
  return html`<a
360
- href="${this.baseNavigationUrl}${collection
361
- ? this.collectionPagePath
362
- : '/details/'}${encodeURI(identifier)}"
349
+ href="${this.baseNavigationUrl}/details/${encodeURI(identifier)}"
363
350
  >${DOMPurify.sanitize(linkText)}</a
364
351
  >`;
365
352
  }
@@ -377,7 +364,7 @@ export class TileList extends LitElement {
377
364
  case 'account':
378
365
  return nothing;
379
366
  default:
380
- return `${this.baseNavigationUrl}${this.collectionPagePath}${encodeURI(
367
+ return `${this.baseNavigationUrl}/details/${encodeURI(
381
368
  this.model.mediatype
382
369
  )}`;
383
370
  }
@@ -411,7 +398,7 @@ export class TileList extends LitElement {
411
398
  promises.push(
412
399
  this.collectionNameCache?.collectionNameFor(collection).then(name => {
413
400
  newCollectionLinks.push(
414
- this.detailsLink(collection, name ?? collection, true)
401
+ this.detailsLink(collection, name ?? collection)
415
402
  );
416
403
  })
417
404
  );
@@ -56,8 +56,6 @@ export class TileDispatcher
56
56
  /** Whether this tile should include a hover pane at all (for applicable tile modes) */
57
57
  @property({ type: Boolean }) enableHoverPane = false;
58
58
 
59
- @property({ type: String }) collectionPagePath: string = '/details/';
60
-
61
59
  private hoverPaneController?: HoverPaneControllerInterface;
62
60
 
63
61
  @query('#container')
@@ -142,14 +140,9 @@ export class TileDispatcher
142
140
  private get linkTileHref() {
143
141
  // Use the server-specified href if available.
144
142
  // Otherwise, construct a details page URL from the item identifier.
145
- if (this.model?.href) {
146
- return `${this.baseNavigationUrl}${this.model?.href}`;
147
- }
148
-
149
- const isCollection = this.model?.mediatype === 'collection';
150
- return `${this.baseNavigationUrl}${
151
- isCollection ? this.collectionPagePath : '/details/'
152
- }${this.model?.identifier}`;
143
+ return this.model?.href
144
+ ? `${this.baseNavigationUrl}${this.model?.href}`
145
+ : `${this.baseNavigationUrl}/details/${this.model?.identifier}`;
153
146
  }
154
147
 
155
148
  /**
@@ -273,7 +266,6 @@ export class TileDispatcher
273
266
  case 'list-compact':
274
267
  return html`<tile-list-compact
275
268
  .model=${model}
276
- .collectionPagePath=${this.collectionPagePath}
277
269
  .currentWidth=${currentWidth}
278
270
  .currentHeight=${currentHeight}
279
271
  .baseNavigationUrl=${baseNavigationUrl}
@@ -286,7 +278,6 @@ export class TileDispatcher
286
278
  case 'list-detail':
287
279
  return html`<tile-list
288
280
  .model=${model}
289
- .collectionPagePath=${this.collectionPagePath}
290
281
  .collectionNameCache=${this.collectionNameCache}
291
282
  .currentWidth=${currentWidth}
292
283
  .currentHeight=${currentHeight}
@@ -945,7 +945,6 @@ describe('Collection Browser', () => {
945
945
  const searchService = new MockSearchService();
946
946
  const el = await fixture<CollectionBrowser>(
947
947
  html`<collection-browser
948
- .log=${true}
949
948
  .searchService=${searchService}
950
949
  ></collection-browser>`
951
950
  );
@@ -961,6 +960,34 @@ describe('Collection Browser', () => {
961
960
  expect(spy.secondCall.firstArg?.detail?.loading).to.equal(false);
962
961
  });
963
962
 
963
+ it('collapses extra set of quotes around href field', async () => {
964
+ const searchService = new MockSearchService();
965
+ const el = await fixture<CollectionBrowser>(
966
+ html`<collection-browser
967
+ .searchService=${searchService}
968
+ .baseNavigationUrl=${''}
969
+ ></collection-browser>`
970
+ );
971
+
972
+ el.baseQuery = 'extra-quoted-href';
973
+ await el.updateComplete;
974
+ await el.initialSearchComplete;
975
+ await el.updateComplete;
976
+ await nextTick();
977
+
978
+ const infiniteScroller = el.shadowRoot?.querySelector('infinite-scroller');
979
+ expect(infiniteScroller).to.exist;
980
+
981
+ const firstResult =
982
+ infiniteScroller!.shadowRoot?.querySelector('tile-dispatcher');
983
+ expect(firstResult).to.exist;
984
+
985
+ // Original href q param starts/ends with %22%22, but should be collapsed to %22 before render
986
+ expect(
987
+ firstResult!.shadowRoot?.querySelector('a[href]')?.getAttribute('href')
988
+ ).to.equal('/details/foo?q=%22quoted+query%22');
989
+ });
990
+
964
991
  it('scrolls to page', async () => {
965
992
  const searchService = new MockSearchService();
966
993
  const el = await fixture<CollectionBrowser>(
@@ -4,6 +4,7 @@ import {
4
4
  ItemHit,
5
5
  SearchResponse,
6
6
  SearchServiceError,
7
+ TextHit,
7
8
  } from '@internetarchive/search-service';
8
9
  import { SearchServiceErrorType } from '@internetarchive/search-service/dist/src/search-service-error';
9
10
 
@@ -475,6 +476,42 @@ export const getMockSuccessMultiLineDescription: () => Result<
475
476
  },
476
477
  });
477
478
 
479
+ export const getMockSuccessExtraQuotedHref: () => Result<
480
+ SearchResponse,
481
+ SearchServiceError
482
+ > = () => ({
483
+ success: {
484
+ request: {
485
+ clientParameters: {
486
+ user_query: 'extra-quoted-href',
487
+ sort: [],
488
+ },
489
+ finalizedParameters: {
490
+ user_query: 'extra-quoted-href',
491
+ sort: [],
492
+ },
493
+ },
494
+ rawResponse: {},
495
+ response: {
496
+ totalResults: 1,
497
+ returnedCount: 1,
498
+ results: [
499
+ new TextHit({
500
+ fields: {
501
+ identifier: 'foo',
502
+ title: 'Foo',
503
+ __href__: '/details/foo?q=%22%22quoted+query%22%22',
504
+ },
505
+ }),
506
+ ],
507
+ },
508
+ responseHeader: {
509
+ succeeded: true,
510
+ query_time: 0,
511
+ },
512
+ },
513
+ });
514
+
478
515
  export const getMockErrorResult: () => Result<
479
516
  SearchResponse,
480
517
  SearchServiceError
@@ -21,6 +21,7 @@ import {
21
21
  getMockMalformedResult,
22
22
  getMockSuccessWithCollectionTitles,
23
23
  getMockSuccessWithCollectionAggregations,
24
+ getMockSuccessExtraQuotedHref,
24
25
  } from './mock-search-responses';
25
26
 
26
27
  const responses: Record<
@@ -37,6 +38,7 @@ const responses: Record<
37
38
  'first-creator': getMockSuccessFirstCreatorResult,
38
39
  'collection-titles': getMockSuccessWithCollectionTitles,
39
40
  'collection-aggregations': getMockSuccessWithCollectionAggregations,
41
+ 'extra-quoted-href': getMockSuccessExtraQuotedHref,
40
42
  error: getMockErrorResult,
41
43
  malformed: getMockMalformedResult,
42
44
  };