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

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,3 @@
1
- # 5.0.0-40
2
- Fix: Better search highlights @cdrini
3
- Dev: update lit 2 components @iisa
4
- Dev: update lit @renovate
5
-
6
1
  # 5.0.0-39
7
2
  Fix: Performance improvements to scroll/zooming when text layer is larger @cdrini
8
3
  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",
3
+ "version": "5.0.0-40-a1",
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.2.2"
41
+ "lit": "^2.1.3"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@babel/core": "7.17.5",
@@ -1,7 +1,6 @@
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 */
5
4
 
6
5
  let searchState = {
7
6
  query: '',
@@ -29,10 +28,10 @@ export default class SearchProvider {
29
28
  this.bindEventListeners = this.bindEventListeners.bind(this);
30
29
  this.getMenuDetails = this.getMenuDetails.bind(this);
31
30
  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} */
36
35
  this.bookreader = bookreader;
37
36
  this.icon = html`<ia-icon-search style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-search>`;
38
37
  this.label = 'Search inside';
@@ -175,10 +174,13 @@ export default class SearchProvider {
175
174
  `;
176
175
  }
177
176
 
178
- /**
179
- * @param {{ detail: {match: SearchInsideMatch} }} param0
180
- */
181
177
  onSearchResultsClicked({ detail }) {
182
- this.bookreader._searchPluginGoToResult(detail.match.matchIndex);
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);
183
185
  }
184
186
  }
@@ -103,10 +103,6 @@ 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");
110
106
  return rect;
111
107
  }
112
108
 
@@ -115,9 +111,8 @@ export function boxToSVGRect({ l: left, r: right, b: bottom, t: top }) {
115
111
  * @param {Array<{ l: number, r: number, b: number, t: number }>} boxes
116
112
  * @param {PageModel} page
117
113
  * @param {HTMLElement} containerEl
118
- * @param {string[]} [rectClasses] CSS classes to add to the rects
119
114
  */
120
- export function renderBoxesInPageContainerLayer(layerClass, boxes, page, containerEl, rectClasses = null) {
115
+ export function renderBoxesInPageContainerLayer(layerClass, boxes, page, containerEl) {
121
116
  const mountedSvg = containerEl.querySelector(`.${layerClass}`);
122
117
  // Create the layer if it's not there
123
118
  const svg = mountedSvg || createSVGPageLayer(page, layerClass);
@@ -127,12 +122,5 @@ export function renderBoxesInPageContainerLayer(layerClass, boxes, page, contain
127
122
  if (imgEl) $(svg).insertAfter(imgEl);
128
123
  else $(svg).prependTo(containerEl);
129
124
  }
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
- }
125
+ boxes.forEach(box => svg.appendChild(boxToSVGRect(box)));
138
126
  }
@@ -238,27 +238,3 @@ 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,8 +1006,10 @@ BookReader.prototype.jumpToPage = function(pageNum) {
1006
1006
  * @param {PageIndex} index
1007
1007
  */
1008
1008
  BookReader.prototype._isIndexDisplayed = function(index) {
1009
- return this.displayedIndices ? this.displayedIndices.includes(index) :
1010
- this.currentIndex() == 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;
1011
1013
  };
1012
1014
 
1013
1015
  /**
@@ -31,22 +31,9 @@
31
31
  pointer-events: none;
32
32
 
33
33
  rect {
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
- }
34
+ fill: #0000ff;
35
+ fill-opacity: 0.2;
36
+ animation: hiliteFadeIn .2s;
50
37
  }
51
38
  }
52
39
 
@@ -220,8 +207,8 @@
220
207
  }
221
208
  }
222
209
 
223
- @keyframes highlightFocus {
224
- to { stroke-width: 20px; }
210
+ @keyframes hiliteFadeIn {
211
+ from { fill-opacity: 0; }
225
212
  }
226
213
 
227
214
  /* Mid size breakpoint */
@@ -1,4 +1,3 @@
1
- // @ts-check
2
1
  /* global BookReader */
3
2
  /**
4
3
  * Plugin for Archive.org book search
@@ -24,7 +23,6 @@
24
23
  * @event BookReader:SearchCanceled - When no results found. Receives
25
24
  * `instance`
26
25
  */
27
- import { poll } from '../../BookReader/utils.js';
28
26
  import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
29
27
  import SearchView from './view.js';
30
28
  /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
@@ -114,14 +112,7 @@ BookReader.prototype._createPageContainer = (function (super_) {
114
112
  const pageContainer = super_.call(this, index);
115
113
  if (this.enableSearch && pageContainer.page && index in this._searchBoxesByIndex) {
116
114
  const pageIndex = pageContainer.page.index;
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
- );
115
+ renderBoxesInPageContainerLayer('searchHiliteLayer', this._searchBoxesByIndex[pageIndex], pageContainer.page, pageContainer.$container[0]);
125
116
  }
126
117
  return pageContainer;
127
118
  };
@@ -189,7 +180,7 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
189
180
 
190
181
  const url = `${baseUrl}${paramStr}`;
191
182
 
192
- const callSearchResultsCallback = (searchInsideResults) => {
183
+ const processSearchResults = (searchInsideResults) => {
193
184
  if (this.searchCancelled) {
194
185
  return;
195
186
  }
@@ -209,7 +200,7 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
209
200
  };
210
201
 
211
202
  this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
212
- callSearchResultsCallback(await $.ajax({
203
+ return processSearchResults(await $.ajax({
213
204
  url: url,
214
205
  dataType: 'jsonp',
215
206
  cache: true,
@@ -251,12 +242,10 @@ BookReader.prototype.cancelSearchRequest = function () {
251
242
  * @property {number} b
252
243
  * @property {number} t
253
244
  * @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.
255
245
  */
256
246
 
257
247
  /**
258
248
  * @typedef {object} SearchInsideMatch
259
- * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
260
249
  * @property {string} text
261
250
  * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
262
251
  */
@@ -270,27 +259,19 @@ BookReader.prototype.cancelSearchRequest = function () {
270
259
 
271
260
  /**
272
261
  * Search Results return handler
262
+ * @callback
273
263
  * @param {SearchInsideResults} results
274
264
  * @param {object} options
275
265
  * @param {boolean} options.goToFirstResult
276
266
  */
277
267
  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
- }
288
268
  this.searchResults = results || [];
289
269
 
290
270
  this.updateSearchHilites();
291
271
  this.removeProgressPopup();
292
272
  if (options.goToFirstResult) {
293
- this._searchPluginGoToResult(0);
273
+ const pageIndex = this._models.book.leafNumToIndex(results.matches[0].par[0].page);
274
+ this._searchPluginGoToResult(pageIndex);
294
275
  }
295
276
  this.trigger('SearchCallback', { results, options, instance: this });
296
277
  };
@@ -335,7 +316,7 @@ BookReader.prototype._BRSearchCallbackError = function(results) {
335
316
  BookReader.prototype.updateSearchHilites = function() {
336
317
  /** @type {SearchInsideMatch[]} */
337
318
  const matches = this.searchResults?.matches || [];
338
- /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
319
+ /** @type { {[pageIndex: number]: SearchInsideMatch[]} } */
339
320
  const boxesByIndex = {};
340
321
 
341
322
  // Clear any existing svg layers
@@ -345,8 +326,8 @@ BookReader.prototype.updateSearchHilites = function() {
345
326
  for (const match of matches) {
346
327
  for (const box of match.par[0].boxes) {
347
328
  const pageIndex = this.leafNumToIndex(box.page);
348
- const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
349
- pageBoxes.push(box);
329
+ const pageMatches = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
330
+ pageMatches.push(box);
350
331
  }
351
332
  }
352
333
 
@@ -355,9 +336,7 @@ BookReader.prototype.updateSearchHilites = function() {
355
336
  const pageIndex = parseFloat(pageIndexString);
356
337
  const page = this._models.book.getPage(pageIndex);
357
338
  const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
358
- for (const container of pageContainers) {
359
- renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
360
- }
339
+ pageContainers.forEach(container => renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container));
361
340
  }
362
341
 
363
342
  this._searchBoxesByIndex = boxesByIndex;
@@ -375,14 +354,11 @@ BookReader.prototype.removeSearchHilites = function() {
375
354
  * Goes to the page specified. If the page is not viewable, tries to load the page
376
355
  * FIXME Most of this logic is IA specific, and should be less integrated into here
377
356
  * or at least more configurable.
378
- * @param {number} matchIndex
357
+ * @param {PageIndex} pageIndex
379
358
  */
380
- BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
381
- const match = this.searchResults?.matches[matchIndex];
382
- const pageIndex = this.leafNumToIndex(match.par[0].page);
359
+ BookReader.prototype._searchPluginGoToResult = async function (pageIndex) {
383
360
  const { book } = this._models;
384
361
  const page = book.getPage(pageIndex);
385
- const onNearbyPage = Math.abs(this.currentIndex() - pageIndex) < 3;
386
362
  let makeUnviewableAtEnd = false;
387
363
  if (!page.isViewable) {
388
364
  const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
@@ -403,35 +379,13 @@ BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
403
379
  }
404
380
  }
405
381
  /* this updates the URL */
406
- if (!this._isIndexDisplayed(pageIndex)) {
407
- this.suppressFragmentChange = false;
408
- this.jumpToIndex(pageIndex);
409
- }
382
+ this.suppressFragmentChange = false;
383
+ this.jumpToIndex(pageIndex);
410
384
 
411
385
  // Reset it to unviewable if it wasn't resolved
412
386
  if (makeUnviewableAtEnd) {
413
387
  book.getPage(pageIndex).makeViewable(false);
414
388
  }
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
- }
435
389
  };
436
390
 
437
391
  /**
@@ -260,6 +260,7 @@ class SearchView {
260
260
  <div>${uiStringPage} ${pageNumber}</div>
261
261
  </div>
262
262
  `)
263
+ .data({ pageIndex })
263
264
  .appendTo(this.br.$('.BRnavline'))
264
265
  .on("mouseenter", (event) => {
265
266
  // remove from other markers then turn on just for this
@@ -276,7 +277,12 @@ class SearchView {
276
277
  $(event.target).addClass('front');
277
278
  })
278
279
  .on("mouseleave", (event) => $(event.target).removeClass('front'))
279
- .on("click", () => { this.br._searchPluginGoToResult(match.matchIndex); });
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));
280
286
  });
281
287
  }
282
288
 
@@ -1,5 +1,5 @@
1
1
  import AbstractTTSEngine from './AbstractTTSEngine.js';
2
- import { sleep } from '../../BookReader/utils.js';
2
+ import { sleep } from './utils.js';
3
3
  /* global soundManager */
4
4
  import 'soundmanager2';
5
5
  import 'jquery.browser';
@@ -1,7 +1,6 @@
1
1
  /* global br */
2
2
  import { isChrome, isFirefox } from '../../util/browserSniffing.js';
3
- import { promisifyEvent, isAndroid } from './utils.js';
4
- import { sleep } from '../../BookReader/utils.js';
3
+ import { sleep, promisifyEvent, isAndroid } from './utils.js';
5
4
  import AbstractTTSEngine from './AbstractTTSEngine.js';
6
5
  /** @typedef {import("./AbstractTTSEngine.js").PageChunk} PageChunk */
7
6
  /** @typedef {import("./AbstractTTSEngine.js").AbstractTTSSound} AbstractTTSSound */
@@ -26,6 +26,15 @@ 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
+
29
38
  /**
30
39
  * Checks whether the current browser is on android
31
40
  * @param {string} [userAgent]
@@ -1,5 +1,5 @@
1
1
  import BookReader from '@/src/BookReader';
2
- import { sleep } from '@/src/BookReader/utils.js';
2
+ import { sleep } from '@/src/plugins/tts/utils';
3
3
  import sinon from 'sinon';
4
4
 
5
5
  beforeAll(() => {
@@ -178,15 +178,6 @@ 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
-
190
181
  test('Handles no boxes', () => {
191
182
  const container = document.createElement('div');
192
183
  const page = { width: 100, height: 200 };
@@ -1,5 +1,4 @@
1
1
  import sinon from 'sinon';
2
- import { afterEventLoop } from '../utils.js';
3
2
  import {
4
3
  clamp,
5
4
  cssPercentage,
@@ -9,10 +8,8 @@ import {
9
8
  escapeHTML,
10
9
  getActiveElement,
11
10
  isInputActive,
12
- poll,
13
11
  polyfillCustomEvent,
14
12
  PolyfilledCustomEvent,
15
- sleep,
16
13
  } from '@/src/BookReader/utils.js';
17
14
 
18
15
  test('clamp function returns Math.min(Math.max(value, min), max)', () => {
@@ -137,50 +134,3 @@ describe('PolyfilledCustomEvent', () => {
137
134
  expect(initCustomEventSpy.callCount).toBe(1);
138
135
  });
139
136
  });
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,6 +66,31 @@ 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
+
69
94
  describe('toISO6391', () => {
70
95
  const { toISO6391 } = utils;
71
96
 
@@ -23,6 +23,21 @@ 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
+ });
26
41
  describe('Search request life cycles', () => {
27
42
  it('Event: catches `BookReader:SearchStarted`', async() => {
28
43
  const provider = new searchProvider({
@@ -91,6 +106,7 @@ describe('Search Provider', () => {
91
106
  _searchPluginGoToResult: sinon.fake()
92
107
  }
93
108
  });
109
+ sinon.spy(provider, 'advanceToPage');
94
110
 
95
111
  const searchResultStub = {
96
112
  match: { par: [{ text: 'foo', page: 3 }] },
@@ -100,6 +116,7 @@ describe('Search Provider', () => {
100
116
  { detail: searchResultStub })
101
117
  );
102
118
 
119
+ expect(provider.advanceToPage.callCount).to.equal(1);
103
120
  expect(provider.bookreader._searchPluginGoToResult.callCount).to.equal(1);
104
121
  });
105
122
  });