@internetarchive/bookreader 5.0.0-40-a1 → 5.0.0-42-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/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 +5 -5
- 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-
|
3
|
+
"version": "5.0.0-42-a1",
|
4
4
|
"description": "The Internet Archive BookReader.",
|
5
5
|
"repository": {
|
6
6
|
"type": "git",
|
@@ -26,7 +26,7 @@
|
|
26
26
|
"private": false,
|
27
27
|
"dependencies": {
|
28
28
|
"@internetarchive/ia-activity-indicator": "^0.0.2",
|
29
|
-
"@internetarchive/ia-item-navigator": "^1.0.
|
29
|
+
"@internetarchive/ia-item-navigator": "^1.0.1-a1",
|
30
30
|
"@internetarchive/ia-sharing-options": "^1.0.1",
|
31
31
|
"@internetarchive/icon-bookmark": "^1.3.2",
|
32
32
|
"@internetarchive/icon-dl": "^1.3.3",
|
@@ -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",
|
@@ -46,7 +46,7 @@
|
|
46
46
|
"@babel/plugin-proposal-class-properties": "7.16.7",
|
47
47
|
"@babel/plugin-proposal-decorators": "7.17.2",
|
48
48
|
"@babel/preset-env": "7.16.11",
|
49
|
-
"@open-wc/testing": "^3.1.
|
49
|
+
"@open-wc/testing": "^3.1.3",
|
50
50
|
"@open-wc/testing-karma": "^4.0.9",
|
51
51
|
"@types/jest": "^27.4.1",
|
52
52
|
"@webcomponents/webcomponentsjs": "^2.6.0",
|
@@ -76,7 +76,7 @@
|
|
76
76
|
"sinon": "^12.0.1",
|
77
77
|
"soundmanager2": "2.97.20170602",
|
78
78
|
"svgo": "2.4.0",
|
79
|
-
"testcafe": "^1.18.
|
79
|
+
"testcafe": "^1.18.6",
|
80
80
|
"testcafe-browser-provider-browserstack": "^1.13.2-alpha.1",
|
81
81
|
"webpack": "5.51.1",
|
82
82
|
"webpack-cli": "4.8.0"
|
@@ -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
|
});
|