@internetarchive/bookreader 5.0.0-40-a1 → 5.0.0-40

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 5.0.0-40
2
+ Fix: Better search highlights @cdrini
3
+ Dev: update lit 2 components @iisa
4
+ Dev: update lit @renovate
5
+
1
6
  # 5.0.0-39
2
7
  Fix: Performance improvements to scroll/zooming when text layer is larger @cdrini
3
8
  Fix: Update zoom in/out icons to match iconochive glyphs @pezvi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-40-a1",
3
+ "version": "5.0.0-40",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "@internetarchive/icon-visual-adjustment": "^1.3.2",
39
39
  "@internetarchive/modal-manager": "^0.2.1",
40
40
  "@internetarchive/shared-resize-observer": "^0.2.0",
41
- "lit": "^2.1.3"
41
+ "lit": "^2.2.2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@babel/core": "7.17.5",
@@ -1,6 +1,7 @@
1
1
  import { html, nothing } from 'lit';
2
2
  import '@internetarchive/icon-search/icon-search';
3
3
  import './search-results';
4
+ /** @typedef {import('@/src/plugins/search/plugin.search.js').SearchInsideMatch} SearchInsideMatch */
4
5
 
5
6
  let searchState = {
6
7
  query: '',
@@ -28,10 +29,10 @@ export default class SearchProvider {
28
29
  this.bindEventListeners = this.bindEventListeners.bind(this);
29
30
  this.getMenuDetails = this.getMenuDetails.bind(this);
30
31
  this.getComponent = this.getComponent.bind(this);
31
- this.advanceToPage = this.advanceToPage.bind(this);
32
32
  this.updateMenu = this.updateMenu.bind(this);
33
33
 
34
34
  this.onProviderChange = onProviderChange;
35
+ /** @type {import('@/src/BookReader.js').default} */
35
36
  this.bookreader = bookreader;
36
37
  this.icon = html`<ia-icon-search style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-search>`;
37
38
  this.label = 'Search inside';
@@ -174,13 +175,10 @@ export default class SearchProvider {
174
175
  `;
175
176
  }
176
177
 
178
+ /**
179
+ * @param {{ detail: {match: SearchInsideMatch} }} param0
180
+ */
177
181
  onSearchResultsClicked({ detail }) {
178
- const page = detail.match.par[0].page;
179
- this.advanceToPage(page);
180
- }
181
-
182
- advanceToPage(leaf) {
183
- const page = this.bookreader.leafNumToIndex(leaf);
184
- this.bookreader._searchPluginGoToResult(page);
182
+ this.bookreader._searchPluginGoToResult(detail.match.matchIndex);
185
183
  }
186
184
  }
@@ -103,6 +103,10 @@ export function boxToSVGRect({ l: left, r: right, b: bottom, t: top }) {
103
103
  rect.setAttribute("y", top.toString());
104
104
  rect.setAttribute("width", (right - left).toString());
105
105
  rect.setAttribute("height", (bottom - top).toString());
106
+
107
+ // Some style; corner radius 4px. Can't set this in CSS yet
108
+ rect.setAttribute("rx", "4");
109
+ rect.setAttribute("ry", "4");
106
110
  return rect;
107
111
  }
108
112
 
@@ -111,8 +115,9 @@ export function boxToSVGRect({ l: left, r: right, b: bottom, t: top }) {
111
115
  * @param {Array<{ l: number, r: number, b: number, t: number }>} boxes
112
116
  * @param {PageModel} page
113
117
  * @param {HTMLElement} containerEl
118
+ * @param {string[]} [rectClasses] CSS classes to add to the rects
114
119
  */
115
- export function renderBoxesInPageContainerLayer(layerClass, boxes, page, containerEl) {
120
+ export function renderBoxesInPageContainerLayer(layerClass, boxes, page, containerEl, rectClasses = null) {
116
121
  const mountedSvg = containerEl.querySelector(`.${layerClass}`);
117
122
  // Create the layer if it's not there
118
123
  const svg = mountedSvg || createSVGPageLayer(page, layerClass);
@@ -122,5 +127,12 @@ export function renderBoxesInPageContainerLayer(layerClass, boxes, page, contain
122
127
  if (imgEl) $(svg).insertAfter(imgEl);
123
128
  else $(svg).prependTo(containerEl);
124
129
  }
125
- boxes.forEach(box => svg.appendChild(boxToSVGRect(box)));
130
+
131
+ for (const [i, box] of boxes.entries()) {
132
+ const rect = boxToSVGRect(box);
133
+ if (rectClasses) {
134
+ rect.setAttribute('class', rectClasses[i]);
135
+ }
136
+ svg.appendChild(rect);
137
+ }
126
138
  }
@@ -238,3 +238,27 @@ export function arrEquals(arr1, arr2) {
238
238
  export function arrChanged(arr1, arr2) {
239
239
  return arr1 && arr2 && !arrEquals(arr1, arr2);
240
240
  }
241
+
242
+ /**
243
+ * Waits the provided number of ms and then resolves with a promise
244
+ * @param {number} ms
245
+ **/
246
+ export async function sleep(ms) {
247
+ return new Promise(resolve => setTimeout(resolve, ms));
248
+ }
249
+
250
+ /**
251
+ * @template T
252
+ * @param {function(): T} fn
253
+ * @param {Object} options
254
+ * @param {function(T): boolean} [options.until]
255
+ * @return {T | undefined}
256
+ */
257
+ export async function poll(fn, { step = 50, timeout = 500, until = val => Boolean(val), _sleep = sleep } = {}) {
258
+ const startTime = Date.now();
259
+ while (Date.now() - startTime < timeout) {
260
+ const result = fn();
261
+ if (until(result)) return result;
262
+ await _sleep(step);
263
+ }
264
+ }
package/src/BookReader.js CHANGED
@@ -1006,10 +1006,8 @@ BookReader.prototype.jumpToPage = function(pageNum) {
1006
1006
  * @param {PageIndex} index
1007
1007
  */
1008
1008
  BookReader.prototype._isIndexDisplayed = function(index) {
1009
- // One up "caches" pages +- current, so exclude those in the test.
1010
- return this.constMode1up == this.mode ? this.displayedIndices.slice(1, -1).includes(index) :
1011
- this.displayedIndices ? this.displayedIndices.includes(index) :
1012
- this.currentIndex() == index;
1009
+ return this.displayedIndices ? this.displayedIndices.includes(index) :
1010
+ this.currentIndex() == index;
1013
1011
  };
1014
1012
 
1015
1013
  /**
@@ -31,9 +31,22 @@
31
31
  pointer-events: none;
32
32
 
33
33
  rect {
34
- fill: #0000ff;
35
- fill-opacity: 0.2;
36
- animation: hiliteFadeIn .2s;
34
+ // Note: Can't use fill-opacity ; safari inexplicably applies that to
35
+ // the outline as well
36
+ fill: rgba(0, 0, 255, 0.2);
37
+ }
38
+ }
39
+
40
+ .searchHiliteLayer rect {
41
+ animation: highlightFocus 600ms 1 reverse;
42
+ stroke: blue;
43
+ stroke-width: 4px;
44
+
45
+ // Sass for loop for nth-child animation delay
46
+ @for $i from 1 through 10 {
47
+ &:nth-child(#{$i}) {
48
+ animation-delay: #{($i - 1) * 50}ms;
49
+ }
37
50
  }
38
51
  }
39
52
 
@@ -207,8 +220,8 @@
207
220
  }
208
221
  }
209
222
 
210
- @keyframes hiliteFadeIn {
211
- from { fill-opacity: 0; }
223
+ @keyframes highlightFocus {
224
+ to { stroke-width: 20px; }
212
225
  }
213
226
 
214
227
  /* Mid size breakpoint */
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  /* global BookReader */
2
3
  /**
3
4
  * Plugin for Archive.org book search
@@ -23,6 +24,7 @@
23
24
  * @event BookReader:SearchCanceled - When no results found. Receives
24
25
  * `instance`
25
26
  */
27
+ import { poll } from '../../BookReader/utils.js';
26
28
  import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
27
29
  import SearchView from './view.js';
28
30
  /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
@@ -112,7 +114,14 @@ BookReader.prototype._createPageContainer = (function (super_) {
112
114
  const pageContainer = super_.call(this, index);
113
115
  if (this.enableSearch && pageContainer.page && index in this._searchBoxesByIndex) {
114
116
  const pageIndex = pageContainer.page.index;
115
- renderBoxesInPageContainerLayer('searchHiliteLayer', this._searchBoxesByIndex[pageIndex], pageContainer.page, pageContainer.$container[0]);
117
+ const boxes = this._searchBoxesByIndex[pageIndex];
118
+ renderBoxesInPageContainerLayer(
119
+ 'searchHiliteLayer',
120
+ boxes,
121
+ pageContainer.page,
122
+ pageContainer.$container[0],
123
+ boxes.map(b => `match-index-${b.matchIndex}`),
124
+ );
116
125
  }
117
126
  return pageContainer;
118
127
  };
@@ -180,7 +189,7 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
180
189
 
181
190
  const url = `${baseUrl}${paramStr}`;
182
191
 
183
- const processSearchResults = (searchInsideResults) => {
192
+ const callSearchResultsCallback = (searchInsideResults) => {
184
193
  if (this.searchCancelled) {
185
194
  return;
186
195
  }
@@ -200,7 +209,7 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
200
209
  };
201
210
 
202
211
  this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
203
- return processSearchResults(await $.ajax({
212
+ callSearchResultsCallback(await $.ajax({
204
213
  url: url,
205
214
  dataType: 'jsonp',
206
215
  cache: true,
@@ -242,10 +251,12 @@ BookReader.prototype.cancelSearchRequest = function () {
242
251
  * @property {number} b
243
252
  * @property {number} t
244
253
  * @property {HTMLDivElement} [div]
254
+ * @property {number} matchIndex This is a fake field! not part of the API response. The index of the match that contains this box in total search results matches.
245
255
  */
246
256
 
247
257
  /**
248
258
  * @typedef {object} SearchInsideMatch
259
+ * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
249
260
  * @property {string} text
250
261
  * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
251
262
  */
@@ -259,19 +270,27 @@ BookReader.prototype.cancelSearchRequest = function () {
259
270
 
260
271
  /**
261
272
  * Search Results return handler
262
- * @callback
263
273
  * @param {SearchInsideResults} results
264
274
  * @param {object} options
265
275
  * @param {boolean} options.goToFirstResult
266
276
  */
267
277
  BookReader.prototype.BRSearchCallback = function(results, options) {
278
+ // Attach matchIndex to a few things to make it easier to identify
279
+ // an active/selected match
280
+ for (const [index, match] of results.matches.entries()) {
281
+ match.matchIndex = index;
282
+ for (const par of match.par) {
283
+ for (const box of par.boxes) {
284
+ box.matchIndex = index;
285
+ }
286
+ }
287
+ }
268
288
  this.searchResults = results || [];
269
289
 
270
290
  this.updateSearchHilites();
271
291
  this.removeProgressPopup();
272
292
  if (options.goToFirstResult) {
273
- const pageIndex = this._models.book.leafNumToIndex(results.matches[0].par[0].page);
274
- this._searchPluginGoToResult(pageIndex);
293
+ this._searchPluginGoToResult(0);
275
294
  }
276
295
  this.trigger('SearchCallback', { results, options, instance: this });
277
296
  };
@@ -316,7 +335,7 @@ BookReader.prototype._BRSearchCallbackError = function(results) {
316
335
  BookReader.prototype.updateSearchHilites = function() {
317
336
  /** @type {SearchInsideMatch[]} */
318
337
  const matches = this.searchResults?.matches || [];
319
- /** @type { {[pageIndex: number]: SearchInsideMatch[]} } */
338
+ /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
320
339
  const boxesByIndex = {};
321
340
 
322
341
  // Clear any existing svg layers
@@ -326,8 +345,8 @@ BookReader.prototype.updateSearchHilites = function() {
326
345
  for (const match of matches) {
327
346
  for (const box of match.par[0].boxes) {
328
347
  const pageIndex = this.leafNumToIndex(box.page);
329
- const pageMatches = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
330
- pageMatches.push(box);
348
+ const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
349
+ pageBoxes.push(box);
331
350
  }
332
351
  }
333
352
 
@@ -336,7 +355,9 @@ BookReader.prototype.updateSearchHilites = function() {
336
355
  const pageIndex = parseFloat(pageIndexString);
337
356
  const page = this._models.book.getPage(pageIndex);
338
357
  const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
339
- pageContainers.forEach(container => renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container));
358
+ for (const container of pageContainers) {
359
+ renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
360
+ }
340
361
  }
341
362
 
342
363
  this._searchBoxesByIndex = boxesByIndex;
@@ -354,11 +375,14 @@ BookReader.prototype.removeSearchHilites = function() {
354
375
  * Goes to the page specified. If the page is not viewable, tries to load the page
355
376
  * FIXME Most of this logic is IA specific, and should be less integrated into here
356
377
  * or at least more configurable.
357
- * @param {PageIndex} pageIndex
378
+ * @param {number} matchIndex
358
379
  */
359
- BookReader.prototype._searchPluginGoToResult = async function (pageIndex) {
380
+ BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
381
+ const match = this.searchResults?.matches[matchIndex];
382
+ const pageIndex = this.leafNumToIndex(match.par[0].page);
360
383
  const { book } = this._models;
361
384
  const page = book.getPage(pageIndex);
385
+ const onNearbyPage = Math.abs(this.currentIndex() - pageIndex) < 3;
362
386
  let makeUnviewableAtEnd = false;
363
387
  if (!page.isViewable) {
364
388
  const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
@@ -379,13 +403,35 @@ BookReader.prototype._searchPluginGoToResult = async function (pageIndex) {
379
403
  }
380
404
  }
381
405
  /* this updates the URL */
382
- this.suppressFragmentChange = false;
383
- this.jumpToIndex(pageIndex);
406
+ if (!this._isIndexDisplayed(pageIndex)) {
407
+ this.suppressFragmentChange = false;
408
+ this.jumpToIndex(pageIndex);
409
+ }
384
410
 
385
411
  // Reset it to unviewable if it wasn't resolved
386
412
  if (makeUnviewableAtEnd) {
387
413
  book.getPage(pageIndex).makeViewable(false);
388
414
  }
415
+
416
+ // Scroll/flash in the ui
417
+ const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
418
+ if ($boxes.length) {
419
+ $boxes.css('animation', 'none');
420
+ $boxes[0].scrollIntoView({
421
+ // Only vertically center the highlight if we're in 1up or in full screen. In
422
+ // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
423
+ // center the highlight 🙄 See:
424
+ // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
425
+ // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
426
+ // full-width, and covers up the last line of the highlight.
427
+ block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
428
+ inline: 'center',
429
+ behavior: onNearbyPage ? 'smooth' : 'auto',
430
+ });
431
+ // wait for animation to start
432
+ await new Promise(resolve => setTimeout(resolve, 100));
433
+ $boxes.removeAttr("style");
434
+ }
389
435
  };
390
436
 
391
437
  /**
@@ -260,7 +260,6 @@ class SearchView {
260
260
  <div>${uiStringPage} ${pageNumber}</div>
261
261
  </div>
262
262
  `)
263
- .data({ pageIndex })
264
263
  .appendTo(this.br.$('.BRnavline'))
265
264
  .on("mouseenter", (event) => {
266
265
  // remove from other markers then turn on just for this
@@ -277,12 +276,7 @@ class SearchView {
277
276
  $(event.target).addClass('front');
278
277
  })
279
278
  .on("mouseleave", (event) => $(event.target).removeClass('front'))
280
- .on("click", function (event) {
281
- // closures are nested and deep, using an arrow function breaks references.
282
- // Todo: update to arrow function & clean up closures
283
- // to remove `bind` dependency
284
- this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
285
- }.bind(this));
279
+ .on("click", () => { this.br._searchPluginGoToResult(match.matchIndex); });
286
280
  });
287
281
  }
288
282
 
@@ -1,5 +1,5 @@
1
1
  import AbstractTTSEngine from './AbstractTTSEngine.js';
2
- import { sleep } from './utils.js';
2
+ import { sleep } from '../../BookReader/utils.js';
3
3
  /* global soundManager */
4
4
  import 'soundmanager2';
5
5
  import 'jquery.browser';
@@ -1,6 +1,7 @@
1
1
  /* global br */
2
2
  import { isChrome, isFirefox } from '../../util/browserSniffing.js';
3
- import { sleep, promisifyEvent, isAndroid } from './utils.js';
3
+ import { promisifyEvent, isAndroid } from './utils.js';
4
+ import { sleep } from '../../BookReader/utils.js';
4
5
  import AbstractTTSEngine from './AbstractTTSEngine.js';
5
6
  /** @typedef {import("./AbstractTTSEngine.js").PageChunk} PageChunk */
6
7
  /** @typedef {import("./AbstractTTSEngine.js").AbstractTTSSound} AbstractTTSSound */
@@ -26,15 +26,6 @@ export function approximateWordCount(text) {
26
26
  return m ? m.length : 0;
27
27
  }
28
28
 
29
- /**
30
- * Waits the provided number of ms and then resolves with a promise
31
- * @param {number} ms
32
- * @return {Promise}
33
- */
34
- export function sleep(ms) {
35
- return new Promise(res => setTimeout(res, ms));
36
- }
37
-
38
29
  /**
39
30
  * Checks whether the current browser is on android
40
31
  * @param {string} [userAgent]
@@ -1,5 +1,5 @@
1
1
  import BookReader from '@/src/BookReader';
2
- import { sleep } from '@/src/plugins/tts/utils';
2
+ import { sleep } from '@/src/BookReader/utils.js';
3
3
  import sinon from 'sinon';
4
4
 
5
5
  beforeAll(() => {
@@ -178,6 +178,15 @@ describe('renderBoxesInPageContainerLayer', () => {
178
178
  expect(container.querySelectorAll('.foo rect').length).toBe(3);
179
179
  });
180
180
 
181
+ test('Adds optional classes', () => {
182
+ const container = document.createElement('div');
183
+ const page = { width: 100, height: 200 };
184
+ const boxes = [{l: 1, r: 2, t: 3, b: 4}, {l: 1, r: 2, t: 3, b: 4}, {l: 1, r: 2, t: 3, b: 4}];
185
+ renderBoxesInPageContainerLayer('foo', boxes, page, container, ['match-1', 'match-2', 'match-3']);
186
+ const rects = Array.from(container.querySelectorAll('.foo rect'));
187
+ expect(rects.map(r => r.getAttribute('class'))).toEqual(['match-1', 'match-2', 'match-3']);
188
+ });
189
+
181
190
  test('Handles no boxes', () => {
182
191
  const container = document.createElement('div');
183
192
  const page = { width: 100, height: 200 };
@@ -1,4 +1,5 @@
1
1
  import sinon from 'sinon';
2
+ import { afterEventLoop } from '../utils.js';
2
3
  import {
3
4
  clamp,
4
5
  cssPercentage,
@@ -8,8 +9,10 @@ import {
8
9
  escapeHTML,
9
10
  getActiveElement,
10
11
  isInputActive,
12
+ poll,
11
13
  polyfillCustomEvent,
12
14
  PolyfilledCustomEvent,
15
+ sleep,
13
16
  } from '@/src/BookReader/utils.js';
14
17
 
15
18
  test('clamp function returns Math.min(Math.max(value, min), max)', () => {
@@ -134,3 +137,50 @@ describe('PolyfilledCustomEvent', () => {
134
137
  expect(initCustomEventSpy.callCount).toBe(1);
135
138
  });
136
139
  });
140
+
141
+ describe('poll', () => {
142
+ beforeEach(() => jest.useFakeTimers());
143
+ afterEach(() => jest.useRealTimers());
144
+ test('polls until condition is true', async () => {
145
+ const fakeSleep = sinon.spy((ms) => jest.advanceTimersByTime(ms));
146
+
147
+ const returns = [null, null, 'foo'];
148
+ const check = sinon.spy(() => returns.shift());
149
+ const result = await poll(check, {_sleep: fakeSleep});
150
+ expect(fakeSleep.callCount).toBe(2);
151
+ expect(result).toBe('foo');
152
+ expect(check.callCount).toBe(3);
153
+ });
154
+
155
+ test('times out eventually', async () => {
156
+ const fakeSleep = sinon.spy((ms) => jest.advanceTimersByTime(ms));
157
+
158
+ const check = sinon.stub().returns(null);
159
+ const result = await poll(check, {_sleep: fakeSleep});
160
+ expect(result).toBeUndefined();
161
+ expect(check.callCount).toBe(10);
162
+ });
163
+ });
164
+
165
+ describe('sleep', () => {
166
+ test('Sleep 0 doest not called immediately', async () => {
167
+ const spy = sinon.spy();
168
+ sleep(0).then(spy);
169
+ expect(spy.callCount).toBe(0);
170
+ await afterEventLoop();
171
+ expect(spy.callCount).toBe(1);
172
+ });
173
+
174
+ test('Waits the appropriate ms', async () => {
175
+ const clock = sinon.useFakeTimers();
176
+ const spy = sinon.spy();
177
+ sleep(10).then(spy);
178
+ expect(spy.callCount).toBe(0);
179
+ clock.tick(10);
180
+ expect(spy.callCount).toBe(0);
181
+ clock.restore();
182
+
183
+ await afterEventLoop();
184
+ expect(spy.callCount).toBe(1);
185
+ });
186
+ });
@@ -66,31 +66,6 @@ describe('approximateWordCount', () => {
66
66
  });
67
67
  });
68
68
 
69
- describe('sleep', () => {
70
- const { sleep } = utils;
71
-
72
- test('Sleep 0 doest not called immediately', async () => {
73
- const spy = sinon.spy();
74
- sleep(0).then(spy);
75
- expect(spy.callCount).toBe(0);
76
- await afterEventLoop();
77
- expect(spy.callCount).toBe(1);
78
- });
79
-
80
- test('Waits the appropriate ms', async () => {
81
- const clock = sinon.useFakeTimers();
82
- const spy = sinon.spy();
83
- sleep(10).then(spy);
84
- expect(spy.callCount).toBe(0);
85
- clock.tick(10);
86
- expect(spy.callCount).toBe(0);
87
- clock.restore();
88
-
89
- await afterEventLoop();
90
- expect(spy.callCount).toBe(1);
91
- });
92
- });
93
-
94
69
  describe('toISO6391', () => {
95
70
  const { toISO6391 } = utils;
96
71
 
@@ -23,21 +23,6 @@ describe('Search Provider', () => {
23
23
  expect(provider.menuDetails).to.exist;
24
24
  expect(provider.component).to.exist;
25
25
  });
26
- describe('BR calls', () => {
27
- it('`advanceToPage', () => {
28
- const provider = new searchProvider({
29
- onProviderChange: sinon.fake(),
30
- bookreader: {
31
- leafNumToIndex: sinon.fake(),
32
- _searchPluginGoToResult: sinon.fake()
33
- }
34
- });
35
-
36
- provider.advanceToPage(1);
37
- expect(provider?.bookreader.leafNumToIndex.callCount).to.equal(1);
38
- expect(provider?.bookreader._searchPluginGoToResult.callCount).to.equal(1);
39
- });
40
- });
41
26
  describe('Search request life cycles', () => {
42
27
  it('Event: catches `BookReader:SearchStarted`', async() => {
43
28
  const provider = new searchProvider({
@@ -106,7 +91,6 @@ describe('Search Provider', () => {
106
91
  _searchPluginGoToResult: sinon.fake()
107
92
  }
108
93
  });
109
- sinon.spy(provider, 'advanceToPage');
110
94
 
111
95
  const searchResultStub = {
112
96
  match: { par: [{ text: 'foo', page: 3 }] },
@@ -116,7 +100,6 @@ describe('Search Provider', () => {
116
100
  { detail: searchResultStub })
117
101
  );
118
102
 
119
- expect(provider.advanceToPage.callCount).to.equal(1);
120
103
  expect(provider.bookreader._searchPluginGoToResult.callCount).to.equal(1);
121
104
  });
122
105
  });