@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/BookReader/BookReader.css +39 -6
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +55 -185
- package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +0 -25
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/CHANGELOG.md +5 -0
- package/package.json +2 -2
- package/src/BookNavigator/search/search-provider.js +6 -8
- package/src/BookReader/PageContainer.js +14 -2
- package/src/BookReader/utils.js +24 -0
- package/src/BookReader.js +2 -4
- package/src/css/_BRsearch.scss +18 -5
- package/src/plugins/search/plugin.search.js +60 -14
- package/src/plugins/search/view.js +1 -7
- package/src/plugins/tts/FestivalTTSEngine.js +1 -1
- package/src/plugins/tts/WebTTSEngine.js +2 -1
- package/src/plugins/tts/utils.js +0 -9
- package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +1 -1
- package/tests/jest/BookReader/PageContainer.test.js +9 -0
- package/tests/jest/BookReader/utils.test.js +50 -0
- package/tests/jest/plugins/tts/utils.test.js +0 -25
- package/tests/karma/BookNavigator/search/search-provider.test.js +0 -17
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
}
|
package/src/BookReader/utils.js
CHANGED
@@ -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
|
-
|
1010
|
-
|
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
|
/**
|
package/src/css/_BRsearch.scss
CHANGED
@@ -31,9 +31,22 @@
|
|
31
31
|
pointer-events: none;
|
32
32
|
|
33
33
|
rect {
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
211
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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]:
|
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
|
330
|
-
|
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
|
-
|
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 {
|
378
|
+
* @param {number} matchIndex
|
358
379
|
*/
|
359
|
-
BookReader.prototype._searchPluginGoToResult = async function (
|
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.
|
383
|
-
|
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",
|
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,6 +1,7 @@
|
|
1
1
|
/* global br */
|
2
2
|
import { isChrome, isFirefox } from '../../util/browserSniffing.js';
|
3
|
-
import {
|
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 */
|
package/src/plugins/tts/utils.js
CHANGED
@@ -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]
|
@@ -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
|
});
|