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

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.
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
  });