@internetarchive/bookreader 5.0.0-34 → 5.0.0-37

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.
Files changed (80) hide show
  1. package/.eslintrc.js +1 -11
  2. package/.github/workflows/node.js.yml +3 -3
  3. package/.github/workflows/npm-publish.yml +2 -16
  4. package/BookReader/BookReader.css +1 -1
  5. package/BookReader/BookReader.js +1 -1
  6. package/BookReader/BookReader.js.LICENSE.txt +8 -29
  7. package/BookReader/BookReader.js.map +1 -1
  8. package/BookReader/ia-bookreader-bundle.js +103 -102
  9. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +15 -12
  10. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  11. package/BookReader/plugins/plugin.chapters.js +1 -1
  12. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  13. package/BookReader/plugins/plugin.search.js +1 -1
  14. package/BookReader/plugins/plugin.search.js.map +1 -1
  15. package/BookReader/plugins/plugin.text_selection.js +1 -1
  16. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  17. package/BookReader/plugins/plugin.tts.js +1 -1
  18. package/BookReader/plugins/plugin.tts.js.map +1 -1
  19. package/BookReader/plugins/plugin.url.js +1 -1
  20. package/BookReader/plugins/plugin.url.js.map +1 -1
  21. package/CHANGELOG.md +21 -0
  22. package/README.md +1 -1
  23. package/package.json +7 -10
  24. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  25. package/src/BookNavigator/assets/button-base.js +1 -1
  26. package/src/BookNavigator/assets/ia-logo.js +1 -1
  27. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  28. package/src/BookNavigator/assets/icon_close.js +1 -1
  29. package/src/BookNavigator/assets/icon_sort_asc.js +1 -1
  30. package/src/BookNavigator/assets/icon_sort_desc.js +1 -1
  31. package/src/BookNavigator/assets/icon_sort_neutral.js +1 -1
  32. package/src/BookNavigator/assets/icon_volumes.js +1 -1
  33. package/src/BookNavigator/book-navigator.js +15 -4
  34. package/src/BookNavigator/bookmarks/bookmark-button.js +1 -1
  35. package/src/BookNavigator/bookmarks/bookmark-edit.js +2 -3
  36. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  37. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +1 -1
  38. package/src/BookNavigator/bookmarks/bookmarks-provider.js +1 -1
  39. package/src/BookNavigator/bookmarks/ia-bookmarks.js +30 -34
  40. package/src/BookNavigator/delete-modal-actions.js +1 -1
  41. package/src/BookNavigator/downloads/downloads-provider.js +1 -1
  42. package/src/BookNavigator/downloads/downloads.js +1 -2
  43. package/src/BookNavigator/search/a-search-result.js +2 -3
  44. package/src/BookNavigator/search/search-provider.js +3 -4
  45. package/src/BookNavigator/search/search-results.js +1 -2
  46. package/src/BookNavigator/sharing.js +1 -1
  47. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +1 -1
  48. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  49. package/src/BookNavigator/volumes/volumes-provider.js +1 -1
  50. package/src/BookNavigator/volumes/volumes.js +2 -3
  51. package/src/BookReader/Mode1Up.js +2 -1
  52. package/src/BookReader/Mode1UpLit.js +3 -2
  53. package/src/BookReader/Toolbar/Toolbar.js +2 -2
  54. package/src/BookReader.js +59 -57
  55. package/src/css/_colorbox.scss +2 -2
  56. package/src/ia-bookreader/ia-bookreader.js +5 -2
  57. package/src/plugins/plugin.chapters.js +11 -15
  58. package/src/plugins/plugin.text_selection.js +9 -10
  59. package/src/plugins/search/plugin.search.js +3 -3
  60. package/src/plugins/tts/AbstractTTSEngine.js +31 -34
  61. package/src/plugins/tts/FestivalTTSEngine.js +10 -11
  62. package/src/plugins/tts/PageChunk.js +11 -20
  63. package/src/plugins/tts/PageChunkIterator.js +8 -12
  64. package/src/plugins/tts/WebTTSEngine.js +59 -68
  65. package/src/plugins/tts/plugin.tts.js +16 -10
  66. package/src/plugins/url/UrlPlugin.js +1 -1
  67. package/tests/e2e/base.test.js +7 -4
  68. package/tests/e2e/helpers/params.js +1 -1
  69. package/tests/e2e/viewmode.test.js +30 -30
  70. package/tests/jest/BookReader/Mode1UpLit.test.js +2 -1
  71. package/tests/jest/plugins/plugin.text_selection.test.js +25 -23
  72. package/tests/jest/plugins/search/plugin.search.test.js +12 -20
  73. package/tests/jest/plugins/tts/AbstractTTSEngine.test.js +3 -3
  74. package/tests/jest/plugins/url/UrlPlugin.test.js +15 -0
  75. package/tests/karma/BookNavigator/book-navigator.test.js +9 -0
  76. package/tests/karma/BookNavigator/bookmarks/bookmarks-list.test.js +2 -2
  77. package/tests/karma/BookNavigator/downloads/downloads.test.js +1 -1
  78. package/tests/karma/BookNavigator/volumes/volumes-provider.test.js +3 -3
  79. package/webpack.config.js +1 -1
  80. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
@@ -91,10 +91,10 @@ export default class FestivalTTSEngine extends AbstractTTSEngine {
91
91
  * See https://stackoverflow.com/questions/12206631/html5-audio-cant-play-through-javascript-unless-triggered-manually-once
92
92
  * @return {PromiseLike}
93
93
  */
94
- iOSCaptureUserIntentHack() {
94
+ async iOSCaptureUserIntentHack() {
95
95
  const sound = soundManager.createSound({ url: SILENCE_1MS[this.audioFormat] });
96
- return new Promise(res => sound.play({onfinish: res}))
97
- .then(() => sound.destruct());
96
+ await new Promise(res => sound.play({onfinish: res}));
97
+ sound.destruct();
98
98
  }
99
99
  }
100
100
 
@@ -122,21 +122,20 @@ class FestivalTTSSound {
122
122
  if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
123
123
  onload();
124
124
  },
125
- onresume: () => {
126
- sleep(25).then(() => {
127
- if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
128
- });
125
+ onresume: async () => {
126
+ await sleep(25);
127
+ if (this.rate != 1) this.sound.setPlaybackRate(this.rate);
129
128
  }
130
129
  });
131
130
  return this.sound.load();
132
131
  }
133
132
 
134
- play() {
135
- return new Promise(res => {
133
+ async play() {
134
+ await new Promise(res => {
136
135
  this._finishResolver = res;
137
136
  this.sound.play({ onfinish: res });
138
- })
139
- .then(() => this.sound.destruct());
137
+ });
138
+ this.sound.destruct();
140
139
  }
141
140
 
142
141
  /** @override */
@@ -21,27 +21,18 @@ export default class PageChunk {
21
21
  * @param {number} leafIndex
22
22
  * @return {Promise<PageChunk[]>}
23
23
  */
24
- static fetch(server, bookPath, leafIndex) {
25
- // jquery's ajax "PromiseLike" implementation is inconsistent with
26
- // modern Promises, so convert it to a full promise (it doesn't forward
27
- // a returned promise to the next handler in the chain, which kind of
28
- // defeats the entire point of using promises to avoid "callback hell")
29
- return new Promise((res, rej) => {
30
- $.ajax({
31
- type: 'GET',
32
- url: `https://${server}/BookReader/BookReaderGetTextWrapper.php`,
33
- dataType:'jsonp',
34
- cache: true,
35
- data: {
36
- path: `${bookPath}_djvu.xml`,
37
- page: leafIndex
38
- },
39
- error: rej,
40
- })
41
- .then(chunks => {
42
- res(PageChunk._fromTextWrapperResponse(leafIndex, chunks));
43
- });
24
+ static async fetch(server, bookPath, leafIndex) {
25
+ const chunks = await $.ajax({
26
+ type: 'GET',
27
+ url: `https://${server}/BookReader/BookReaderGetTextWrapper.php`,
28
+ dataType:'jsonp',
29
+ cache: true,
30
+ data: {
31
+ path: `${bookPath}_djvu.xml`,
32
+ page: leafIndex
33
+ }
44
34
  });
35
+ return PageChunk._fromTextWrapperResponse(leafIndex, chunks);
45
36
  }
46
37
 
47
38
  /**
@@ -53,22 +53,18 @@ export default class PageChunkIterator {
53
53
  * in the correct order.
54
54
  * @return {PromiseLike<"__PageChunkIterator.AT_END__" | PageChunk>}
55
55
  */
56
- _nextUncontrolled() {
56
+ async _nextUncontrolled() {
57
57
  if (this._cursor.page == this.pageCount) {
58
58
  return Promise.resolve(PageChunkIterator.AT_END);
59
59
  }
60
-
61
60
  this._recenterBuffer(this._cursor.page);
62
-
63
- return this._fetchPageChunks(this._cursor.page)
64
- .then(chunks => {
65
- if (this._cursor.chunk == chunks.length) {
66
- this._cursor.page++;
67
- this._cursor.chunk = 0;
68
- return this._nextUncontrolled();
69
- }
70
- return chunks[this._cursor.chunk++];
71
- });
61
+ const chunks = await this._fetchPageChunks(this._cursor.page);
62
+ if (this._cursor.chunk == chunks.length) {
63
+ this._cursor.page++;
64
+ this._cursor.chunk = 0;
65
+ return this._nextUncontrolled();
66
+ }
67
+ return chunks[this._cursor.chunk++];
72
68
  }
73
69
 
74
70
  /**
@@ -167,7 +167,7 @@ export class WebTTSSound {
167
167
  * left off.
168
168
  * @return {Promise<void>}
169
169
  */
170
- reload() {
170
+ async reload() {
171
171
  // We'll restore the pause state, so copy it here
172
172
  const wasPaused = this.paused;
173
173
  // Use recent event to determine where we'll restart from
@@ -179,14 +179,12 @@ export class WebTTSSound {
179
179
  }
180
180
 
181
181
  // We can't modify the utterance object, so we have to make a new one
182
- return this.stop()
183
- .then(() => {
184
- this.load();
185
- // Instead of playing and immediately pausing, we don't start playing. Note
186
- // this is a requirement because pause doesn't work consistently across
187
- // browsers.
188
- if (!wasPaused) this.play();
189
- });
182
+ await this.stop();
183
+ this.load();
184
+ // Instead of playing and immediately pausing, we don't start playing. Note
185
+ // this is a requirement because pause doesn't work consistently across
186
+ // browsers.
187
+ if (!wasPaused) this.play();
190
188
  }
191
189
 
192
190
  play() {
@@ -222,15 +220,16 @@ export class WebTTSSound {
222
220
  return endPromise;
223
221
  }
224
222
 
225
- finish() {
226
- this.stop().then(() => this.utterance.dispatchEvent(new Event('finish')));
223
+ async finish() {
224
+ await this.stop();
225
+ this.utterance.dispatchEvent(new Event('finish'));
227
226
  }
228
227
 
229
228
  /**
230
229
  * @override
231
230
  * Will fire a pause event unless already paused
232
231
  **/
233
- pause() {
232
+ async pause() {
234
233
  if (this.paused) return;
235
234
 
236
235
  const pausePromise = promisifyEvent(this.utterance, 'pause');
@@ -246,19 +245,18 @@ export class WebTTSSound {
246
245
  if (pauseMightNotFire) {
247
246
  // wait for it just in case
248
247
  const timeoutPromise = sleep(100).then(() => 'timeout');
249
- Promise.race([pausePromise, timeoutPromise])
250
- .then(result => {
251
- // We got our pause event; nothing to do!
252
- if (result != 'timeout') return;
253
-
254
- this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
255
- // if pause might not work, then we'll stop entirely and restart later
256
- if (pauseMightNotWork) this.stop();
257
- });
248
+ const result = await Promise.race([pausePromise, timeoutPromise]);
249
+ // We got our pause event; nothing to do!
250
+ if (result != 'timeout') return;
251
+
252
+ this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
253
+
254
+ // if pause might not work, then we'll stop entirely and restart later
255
+ if (pauseMightNotWork) this.stop();
258
256
  }
259
257
  }
260
258
 
261
- resume() {
259
+ async resume() {
262
260
  if (!this.started) {
263
261
  this.play();
264
262
  return;
@@ -278,16 +276,15 @@ export class WebTTSSound {
278
276
  speechSynthesis.resume();
279
277
 
280
278
  if (resumeMightNotFire) {
281
- Promise.race([resumePromise, sleep(100).then(() => 'timeout')])
282
- .then(result => {
283
- if (result != 'timeout') return;
284
-
285
- this.utterance.dispatchEvent(new CustomEvent('resume', {}));
286
- if (resumeMightNotWork) {
287
- const reloadPromise = this.reload();
288
- reloadPromise.then(() => this.play());
289
- }
290
- });
279
+ const result = await Promise.race([resumePromise, sleep(100).then(() => 'timeout')]);
280
+
281
+ if (result != 'timeout') return;
282
+
283
+ this.utterance.dispatchEvent(new CustomEvent('resume', {}));
284
+ if (resumeMightNotWork) {
285
+ await this.reload();
286
+ this.play();
287
+ }
291
288
  }
292
289
  }
293
290
 
@@ -308,45 +305,39 @@ export class WebTTSSound {
308
305
  * We avoid this (as described here: https://bugs.chromium.org/p/chromium/issues/detail?id=679437#c15 )
309
306
  * by pausing after 14 seconds and ~instantly resuming.
310
307
  */
311
- _chromePausingBugFix() {
308
+ async _chromePausingBugFix() {
312
309
  const timeoutPromise = sleep(14000).then(() => 'timeout');
313
310
  const pausePromise = promisifyEvent(this.utterance, 'pause').then(() => 'paused');
314
311
  const endPromise = promisifyEvent(this.utterance, 'end').then(() => 'ended');
315
- return Promise.race([timeoutPromise, pausePromise, endPromise])
316
- .then(result => {
317
- if (location.toString().indexOf('_debugReadAloud=true') != -1) {
318
- console.log(`CHROME-PAUSE-HACK: ${result}`);
319
- }
320
- switch (result) {
321
- case 'ended':
322
- // audio was stopped/finished; nothing to do
323
- break;
324
- case 'paused':
325
- // audio was paused; wait for resume
326
- // Chrome won't let you resume the audio if 14s have passed 🤷‍
327
- // We could do the same as before (but resume+pause instead of pause+resume),
328
- // but that means we'd _constantly_ be running in the background. So in that
329
- // case, let's just restart the chunk
330
- Promise.race([
331
- promisifyEvent(this.utterance, 'resume'),
332
- sleep(14000).then(() => 'timeout'),
333
- ])
334
- .then(result => {
335
- result == 'timeout' ? this.reload() : this._chromePausingBugFix();
336
- });
337
- break;
338
- case 'timeout':
339
- // We hit Chrome's secret cut off time. Pause/resume
340
- // to be able to keep TTS-ing
341
- speechSynthesis.pause();
342
- sleep(25)
343
- .then(() => {
344
- speechSynthesis.resume();
345
- this._chromePausingBugFix();
346
- });
347
- break;
348
- }
349
- });
312
+ const result = await Promise.race([timeoutPromise, pausePromise, endPromise]);
313
+ if (location.toString().indexOf('_debugReadAloud=true') != -1) {
314
+ console.log(`CHROME-PAUSE-HACK: ${result}`);
315
+ }
316
+ switch (result) {
317
+ case 'ended':
318
+ // audio was stopped/finished; nothing to do
319
+ break;
320
+ case 'paused':
321
+ // audio was paused; wait for resume
322
+ // Chrome won't let you resume the audio if 14s have passed 🤷‍
323
+ // We could do the same as before (but resume+pause instead of pause+resume),
324
+ // but that means we'd _constantly_ be running in the background. So in that
325
+ // case, let's just restart the chunk
326
+ await Promise.race([
327
+ promisifyEvent(this.utterance, 'resume'),
328
+ sleep(14000).then(() => 'timeout'),
329
+ ]);
330
+ result == 'timeout' ? this.reload() : this._chromePausingBugFix();
331
+ break;
332
+ case 'timeout':
333
+ // We hit Chrome's secret cut off time. Pause/resume
334
+ // to be able to keep TTS-ing
335
+ speechSynthesis.pause();
336
+ await sleep(25);
337
+ speechSynthesis.resume();
338
+ this._chromePausingBugFix();
339
+ break;
340
+ }
350
341
  }
351
342
  }
352
343
 
@@ -261,9 +261,7 @@ BookReader.prototype.ttsStop = function () {
261
261
  BookReader.prototype.ttsBeforeChunkPlay = async function(chunk) {
262
262
  await this.ttsMaybeFlipToIndex(chunk.leafIndex);
263
263
  this.ttsHighlightChunk(chunk);
264
- // This appears not to work; ttsMaybeFlipToIndex causes a scroll to the top of
265
- // the active page :/ Disabling cause the extra scroll just adds an odd jitter.
266
- // this.ttsScrollToChunk(chunk);
264
+ this.ttsScrollToChunk(chunk);
267
265
  };
268
266
 
269
267
  /**
@@ -292,10 +290,7 @@ BookReader.prototype.ttsMaybeFlipToIndex = function (leafIndex) {
292
290
  resolve();
293
291
  } else {
294
292
  this.animationFinishedCallback = resolve;
295
- const mustGoNext = leafIndex > Math.max(this.twoPage.currentIndexR, this.twoPage.currentIndexL);
296
- if (mustGoNext) this.next();
297
- else this.prev();
298
- promise.then(this.ttsMaybeFlipToIndex.bind(this, leafIndex));
293
+ this.jumpToIndex(leafIndex);
299
294
  }
300
295
  }
301
296
 
@@ -329,9 +324,20 @@ BookReader.prototype.ttsHighlightChunk = function(chunk) {
329
324
  * @param {PageChunk} chunk
330
325
  */
331
326
  BookReader.prototype.ttsScrollToChunk = function(chunk) {
332
- if (this.constMode1up != this.mode) return;
333
-
334
- $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`)[0]?.scrollIntoView();
327
+ // It behaves weird if used in thumb mode
328
+ if (this.constModeThumb == this.mode) return;
329
+
330
+ $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]?.scrollIntoView({
331
+ // Only vertically center the highlight if we're in 1up or in full screen. In
332
+ // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
333
+ // center the highlight 🙄 See:
334
+ // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
335
+ // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
336
+ // full-width, and covers up the last line of the highlight.
337
+ block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
338
+ inline: 'center',
339
+ behavior: 'smooth',
340
+ });
335
341
  };
336
342
 
337
343
  // ttsRemoveHilites()
@@ -140,7 +140,7 @@ export class UrlPlugin {
140
140
  const concatenatedPath = urlStrPath !== '/' ? urlStrPath : '';
141
141
  if (this.urlMode == 'history') {
142
142
  if (window.history && window.history.replaceState) {
143
- const newUrlPath = `${this.urlHistoryBasePath}${concatenatedPath}`;
143
+ const newUrlPath = `${this.urlHistoryBasePath}${concatenatedPath}`.trim().replace(/(\/+)/g, '/');
144
144
  window.history.replaceState({}, null, newUrlPath);
145
145
  }
146
146
  } else {
@@ -1,6 +1,6 @@
1
1
  import { runBaseTests } from './helpers/base';
2
2
  import BookReader from './models/BookReader';
3
- import { runDesktopSearchTests } from './helpers/desktopSearch';
3
+ // import { runDesktopSearchTests } from './helpers/desktopSearch';
4
4
  // import { runMobileSearchTests } from './helpers/mobileSearch';
5
5
  import params from './helpers/params';
6
6
 
@@ -21,10 +21,13 @@ ocaids.forEach(ocaid => {
21
21
  fixture `Base Tests for: ${ocaid}`.page `${url}`;
22
22
  runBaseTests(new BookReader());
23
23
 
24
- fixture `Desktop Search Tests for: ${ocaid}`
25
- .page `${url}`;
26
- runDesktopSearchTests(new BookReader());
27
24
 
25
+ // Todo: Re-enable when testing side panel
26
+ // fixture `Desktop Search Tests for: ${ocaid}`
27
+ // .page `${url}`;
28
+ // runDesktopSearchTests(new BookReader());
29
+
30
+ // Todo: deprecated, will remove once mmenu is removed.
28
31
  // fixture `Mobile Search Tests for: ${ocaid}`
29
32
  // .page `${url}`
30
33
  // runMobileSearchTests(new BookReader());
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  class TestParams {
3
3
  baseUrl = process.env.BASE_URL?.replace(/\/+$/, '') ?? 'http://127.0.0.1:8000'
4
- ocaids = process.env.OCAIDS?.split(',') ?? [];
4
+ ocaids = process.env.OCAIDS?.split(',') ?? null;
5
5
  /** Whether the url we're testing is a prod (or near prod) IA url, or a demos url */
6
6
  isIA = new URL(this.baseUrl).hostname.endsWith('archive.org');
7
7
 
@@ -1,36 +1,36 @@
1
- // import { Selector } from 'testcafe';
2
- // import BookReader from './models/BookReader';
3
- // import params from './helpers/params';
1
+ import { Selector } from 'testcafe';
2
+ import BookReader from './models/BookReader';
3
+ import params from './helpers/params';
4
4
 
5
- // fixture `Viewmode carousel`.page `${params.baseUrl}/BookReaderDemo/viewmode-cycle.html`;
5
+ fixture `Viewmode carousel`.page `${params.baseUrl}/BookReaderDemo/demo-internetarchive.html?ocaid=goody`;
6
6
 
7
- // test('Clicking `view mode` cycles through view modes', async t => {
8
- // const { nav } = (new BookReader());
7
+ test('Clicking `view mode` cycles through view modes', async t => {
8
+ const { nav } = (new BookReader());
9
9
 
10
- // // viewmode button only appear on mobile devices
11
- // await t.resizeWindow(400, 800);
12
- // // Flip forward one
13
- // await t.pressKey('right');
10
+ // viewmode button only appear on mobile devices
11
+ await t.resizeWindow(400, 800);
12
+ // Flip forward one
13
+ await t.pressKey('right');
14
14
 
15
- // // 2up to thumb
16
- // await t.click(nav.desktop.viewmode);
17
- // const thumbnailContainer = Selector('.BRmodeThumb');
18
- // await t.expect(thumbnailContainer.visible).ok();
19
- // const thumbImages = thumbnailContainer.find('.BRpageview img');
20
- // await t.expect(thumbImages.count).gt(0);
15
+ // 2up to thumb
16
+ await t.click(nav.desktop.viewmode);
17
+ const thumbnailContainer = Selector('.BRmodeThumb');
18
+ await t.expect(thumbnailContainer.visible).ok();
19
+ const thumbImages = thumbnailContainer.find('.BRpageview img');
20
+ await t.expect(thumbImages.count).gt(0);
21
21
 
22
- // // thumb to 1up
23
- // await t.click(nav.desktop.viewmode);
24
- // const onePageViewContainer = Selector('br-mode-1up');
25
- // await t.expect(onePageViewContainer.visible).ok();
26
- // const onePageImages = onePageViewContainer.find('.BRmode1up .BRpagecontainer');
27
- // // we usually pre-fetch the page in question & 1 before/after it
28
- // await t.expect(onePageImages.count).gte(3);
22
+ // thumb to 1up
23
+ await t.click(nav.desktop.viewmode);
24
+ const onePageViewContainer = Selector('br-mode-1up');
25
+ await t.expect(onePageViewContainer.visible).ok();
26
+ const onePageImages = onePageViewContainer.find('.BRmode1up .BRpagecontainer');
27
+ // we usually pre-fetch the page in question & 1 before/after it
28
+ await t.expect(onePageImages.count).gte(3);
29
29
 
30
- // // 1up to 2up
31
- // await t.click(nav.desktop.viewmode);
32
- // const twoPageContainer = Selector('.BRtwopageview');
33
- // await t.expect(twoPageContainer.visible).ok();
34
- // const twoPageImages = twoPageContainer.find('img.BRpageimage');
35
- // await t.expect(twoPageImages.count).gte(2);
36
- // });
30
+ // 1up to 2up
31
+ await t.click(nav.desktop.viewmode);
32
+ const twoPageContainer = Selector('.BRtwopageview');
33
+ await t.expect(twoPageContainer.visible).ok();
34
+ const twoPageImages = twoPageContainer.find('img.BRpageimage');
35
+ await t.expect(twoPageImages.count).gte(2);
36
+ });
@@ -35,7 +35,8 @@ describe('pageTops', () => {
35
35
  const book = new BookModel(br);
36
36
  const mode = new Mode1UpLit(book, br);
37
37
  document.body.appendChild(mode);
38
- await mode.requestUpdate();
38
+ mode.requestUpdate();
39
+ await mode.updateComplete;
39
40
  expect(mode.pageTops).toEqual({});
40
41
  });
41
42
 
@@ -19,30 +19,32 @@ const FAKE_XML_5COORDS = `<OBJECT data="file://localhost//tmp/derive/goodytwosho
19
19
  const FAKE_XML_EMPTY = '';
20
20
 
21
21
  describe("Generic tests", () => {
22
- let br;
23
- beforeEach(() => {
24
- document.body.innerHTML = '<div id="BookReader">';
25
- br = new BookreaderWithTextSelection({
26
- data: [
27
- [
28
- { width: 800, height: 1200,
29
- uri: '//archive.org/download/BookReader/img/page001.jpg' },
30
- ],
31
- [
32
- { width: 800, height: 1200,
33
- uri: '//archive.org/download/BookReader/img/page002.jpg' },
34
- { width: 800, height: 1200,
35
- uri: '//archive.org/download/BookReader/img/page003.jpg' },
36
- ],
37
- [
38
- { width: 800, height: 1200,
39
- uri: '//archive.org/download/BookReader/img/page004.jpg' },
40
- { width: 800, height: 1200,
41
- uri: '//archive.org/download/BookReader/img/page005.jpg' },
42
- ]
22
+ document.body.innerHTML = '<div id="BookReader">';
23
+ const br = new BookreaderWithTextSelection({
24
+ data: [
25
+ [
26
+ { width: 800, height: 1200,
27
+ uri: '//archive.org/download/BookReader/img/page001.jpg' },
43
28
  ],
44
- });
45
- br.init();
29
+ [
30
+ { width: 800, height: 1200,
31
+ uri: '//archive.org/download/BookReader/img/page002.jpg' },
32
+ { width: 800, height: 1200,
33
+ uri: '//archive.org/download/BookReader/img/page003.jpg' },
34
+ ],
35
+ [
36
+ { width: 800, height: 1200,
37
+ uri: '//archive.org/download/BookReader/img/page004.jpg' },
38
+ { width: 800, height: 1200,
39
+ uri: '//archive.org/download/BookReader/img/page005.jpg' },
40
+ ]
41
+ ],
42
+ });
43
+ br.init();
44
+
45
+ afterEach(() => {
46
+ sinon.restore();
47
+ $('.textSelectionSVG').remove();
46
48
  });
47
49
 
48
50
  test("_createPageContainer overridden function still creates a BRpagecontainer element", () => {
@@ -123,28 +123,24 @@ describe('Plugin: Search', () => {
123
123
  expect(br.options.goToFirstResult).toBeTruthy();
124
124
  });
125
125
 
126
- test('SearchCallback event fires when AJAX search returns results', () => {
126
+ test('SearchCallback event fires when AJAX search returns results', async () => {
127
127
  br.init();
128
- const dfd = br.search('foo');
129
- return dfd.then(() => {
130
- expect(triggeredEvents()).toContain(`${namespace}SearchCallback`);
131
- });
128
+ await br.search('foo');
129
+ expect(triggeredEvents()).toContain(`${namespace}SearchCallback`);
132
130
  });
133
131
 
134
- test('SearchCallbackError event fires when AJAX search returns error', () => {
132
+ test('SearchCallbackError event fires when AJAX search returns error', async () => {
135
133
  $.ajax = jest.fn().mockImplementation(() => {
136
134
  return Promise.resolve({
137
135
  error: true,
138
136
  });
139
137
  });
140
138
  br.init();
141
- const dfd = br.search('foo');
142
- return dfd.then(() => {
143
- expect(triggeredEvents()).toContain(`${namespace}SearchCallbackError`);
144
- });
139
+ await br.search('foo');
140
+ expect(triggeredEvents()).toContain(`${namespace}SearchCallbackError`);
145
141
  });
146
142
 
147
- test('SearchCallbackNotIndexed event fires when AJAX search returns false indexed value', () => {
143
+ test('SearchCallbackNotIndexed event fires when AJAX search returns false indexed value', async () => {
148
144
  $.ajax = jest.fn().mockImplementation(() => {
149
145
  return Promise.resolve({
150
146
  matches: [],
@@ -152,22 +148,18 @@ describe('Plugin: Search', () => {
152
148
  });
153
149
  });
154
150
  br.init();
155
- const dfd = br.search('foo');
156
- return dfd.then(() => {
157
- expect(triggeredEvents()).toContain(`${namespace}SearchCallbackBookNotIndexed`);
158
- });
151
+ await br.search('foo');
152
+ expect(triggeredEvents()).toContain(`${namespace}SearchCallbackBookNotIndexed`);
159
153
  });
160
154
 
161
- test('SearchCallbackEmpty event fires when AJAX search returns no matches', () => {
155
+ test('SearchCallbackEmpty event fires when AJAX search returns no matches', async () => {
162
156
  $.ajax = jest.fn().mockImplementation(() => {
163
157
  return Promise.resolve({
164
158
  matches: [],
165
159
  });
166
160
  });
167
161
  br.init();
168
- const dfd = br.search('foo');
169
- return dfd.then(() => {
170
- expect(triggeredEvents()).toContain(`${namespace}SearchCallbackEmpty`);
171
- });
162
+ await br.search('foo');
163
+ expect(triggeredEvents()).toContain(`${namespace}SearchCallbackEmpty`);
172
164
  });
173
165
  });
@@ -6,7 +6,7 @@ import PageChunkIterator from '@/src/plugins/tts/PageChunkIterator.js';
6
6
 
7
7
  // Skipping because it's flaky. Fix in #672
8
8
  describe.skip('AbstractTTSEngine', () => {
9
- test('stops playing once done', () => {
9
+ test('stops playing once done', async () => {
10
10
  class DummyEngine extends AbstractTTSEngine {
11
11
  getVoices() { return []; }
12
12
  }
@@ -15,8 +15,8 @@ describe.skip('AbstractTTSEngine', () => {
15
15
  const stopStub = sinon.stub(d, 'stop');
16
16
  expect(stopStub.callCount).toBe(0);
17
17
  d.step();
18
- return afterEventLoop()
19
- .then(() => expect(stopStub.callCount).toBe(1));
18
+ await afterEventLoop();
19
+ expect(stopStub.callCount).toBe(1);
20
20
  });
21
21
  });
22
22
 
@@ -170,6 +170,21 @@ describe('UrlPlugin tests', () => {
170
170
  const locationUrl = `${window.location.pathname}${window.location.search}`;
171
171
  expect(locationUrl).toEqual('/details/foo/page/12?q=hello&view=theater');
172
172
  });
173
+
174
+ test('strips leading slash of incoming path name for no double slash', () => {
175
+ const urlPlugin = new UrlPlugin();
176
+ urlPlugin.urlMode = 'history';
177
+
178
+ urlPlugin.urlHistoryBasePath = '/details/SubBookTest/book1/GPORFP/';
179
+ urlPlugin.urlState = {
180
+ "mode": "1up",
181
+ };
182
+
183
+ urlPlugin.setUrlParam('sort', 'title_asc');
184
+ urlPlugin.setUrlParam('mode', 'thumb');
185
+
186
+ expect(window.location.href).toEqual('http://localhost/details/SubBookTest/book1/GPORFP/mode/thumb?sort=title_asc');
187
+ });
173
188
  });
174
189
 
175
190
  });