@internetarchive/bookreader 5.0.0-90 → 5.0.0-92

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 (54) hide show
  1. package/BookReader/BookReader.js +1 -1
  2. package/BookReader/BookReader.js.map +1 -1
  3. package/BookReader/ia-bookreader-bundle.js +2 -2
  4. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  5. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  6. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  7. package/BookReader/plugins/plugin.autoplay.js +1 -1
  8. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  9. package/BookReader/plugins/plugin.chapters.js +2 -2
  10. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  11. package/BookReader/plugins/plugin.iiif.js +1 -1
  12. package/BookReader/plugins/plugin.iiif.js.map +1 -1
  13. package/BookReader/plugins/plugin.resume.js +1 -1
  14. package/BookReader/plugins/plugin.resume.js.map +1 -1
  15. package/BookReader/plugins/plugin.search.js +1 -1
  16. package/BookReader/plugins/plugin.search.js.map +1 -1
  17. package/BookReader/plugins/plugin.text_selection.js +1 -1
  18. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  19. package/BookReader/plugins/plugin.tts.js +1 -1
  20. package/BookReader/plugins/plugin.tts.js.map +1 -1
  21. package/BookReaderDemo/IADemoBr.js +29 -1
  22. package/BookReaderDemo/ia-multiple-volumes-manifest.js +0 -1
  23. package/CHANGELOG.md +28 -0
  24. package/README.md +1 -1
  25. package/package.json +1 -1
  26. package/src/BookNavigator/book-navigator.js +5 -2
  27. package/src/BookNavigator/search/search-provider.js +13 -7
  28. package/src/BookNavigator/sharing.js +1 -1
  29. package/src/BookReader/BookModel.js +5 -4
  30. package/src/BookReader/Toolbar/Toolbar.js +5 -0
  31. package/src/BookReader/options.js +10 -6
  32. package/src/BookReader.js +49 -23
  33. package/src/BookReaderPlugin.js +8 -0
  34. package/src/plugins/plugin.chapters.js +220 -157
  35. package/src/plugins/plugin.text_selection.js +19 -1
  36. package/src/plugins/search/plugin.search.js +330 -376
  37. package/src/plugins/search/view.js +13 -9
  38. package/src/plugins/tts/WebTTSEngine.js +67 -41
  39. package/src/plugins/tts/plugin.tts.js +1 -3
  40. package/src/plugins/tts/utils.js +13 -0
  41. package/src/util/browserSniffing.js +11 -1
  42. package/tests/e2e/helpers/mockSearch.js +1 -1
  43. package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
  44. package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
  45. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
  46. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +70 -0
  47. package/tests/jest/BookReader.test.js +26 -1
  48. package/tests/jest/plugins/plugin.chapters.test.js +56 -58
  49. package/tests/jest/plugins/search/plugin.search.test.js +17 -42
  50. package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
  51. package/tests/jest/plugins/tts/WebTTSEngine.test.js +18 -12
  52. package/tests/jest/plugins/url/plugin.url.test.js +1 -1
  53. package/tests/jest/util/browserSniffing.test.js +9 -3
  54. package/tests/jest/utils.js +4 -1
@@ -1,13 +1,17 @@
1
+ // @ts-check
2
+ /** @typedef {import('@/src/BookReader.js').default} BookReader */
3
+
1
4
  class SearchView {
2
5
  /**
3
6
  * @param {object} params
4
- * @param {object} params.br The BookReader instance
5
- * @param {function} params.cancelSearch callback when a user wants to cancel search
7
+ * @param {BookReader} params.br The BookReader instance
8
+ * @param {function} params.searchCancelledCallback callback when a user wants to cancel search
6
9
  *
7
10
  * @event BookReader:SearchResultsCleared - when the search results nav gets cleared
8
11
  * @event BookReader:ToggleSearchMenu - when search results menu should toggle
9
12
  */
10
13
  constructor({ br, searchCancelledCallback = () => {} }) {
14
+ /** @type {BookReader} */
11
15
  this.br = br;
12
16
  this.matches = [];
13
17
  this.cacheDOMElements();
@@ -40,7 +44,7 @@ class SearchView {
40
44
  }
41
45
 
42
46
  clearSearchFieldAndResults(dispatchEventWhenComplete = true) {
43
- this.br.removeSearchResults();
47
+ this.br._plugins.search.removeSearchResults();
44
48
  this.removeResultPins();
45
49
  this.emptyMatches();
46
50
  this.setQuery('');
@@ -269,15 +273,15 @@ class SearchView {
269
273
  $(event.target).addClass('front');
270
274
  })
271
275
  .on("mouseleave", (event) => $(event.target).removeClass('front'))
272
- .on("click", () => { this.br._searchPluginGoToResult(match.matchIndex); });
276
+ .on("click", () => { this.br._plugins.search.jumpToMatch(match.matchIndex); });
273
277
  });
274
278
  }
275
279
 
276
280
  /**
277
- * @param {boolean} bool
281
+ * @param {boolean} show
278
282
  */
279
- toggleSearchPending(bool) {
280
- if (bool) {
283
+ toggleSearchPending(show = false) {
284
+ if (show) {
281
285
  this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
282
286
  }
283
287
  else {
@@ -375,11 +379,11 @@ class SearchView {
375
379
 
376
380
  handleSearchStarted() {
377
381
  this.emptyMatches();
378
- this.br.removeSearchHilites();
382
+ this.br._plugins.search.removeSearchHilites();
379
383
  this.removeResultPins();
380
384
  this.toggleSearchPending(true);
381
385
  this.teardownSearchNavigation();
382
- this.setQuery(this.br.searchTerm);
386
+ this.setQuery(this.br._plugins.search.searchTerm);
383
387
  }
384
388
 
385
389
  /**
@@ -1,6 +1,6 @@
1
1
  /* global br */
2
2
  import { isChrome, isFirefox } from '../../util/browserSniffing.js';
3
- import { isAndroid } from './utils.js';
3
+ import { isAndroid, DEBUG_READ_ALOUD } from './utils.js';
4
4
  import { promisifyEvent, sleep } from '../../BookReader/utils.js';
5
5
  import AbstractTTSEngine from './AbstractTTSEngine.js';
6
6
  /** @typedef {import("./AbstractTTSEngine.js").PageChunk} PageChunk */
@@ -13,7 +13,7 @@ import AbstractTTSEngine from './AbstractTTSEngine.js';
13
13
  **/
14
14
  export default class WebTTSEngine extends AbstractTTSEngine {
15
15
  static isSupported() {
16
- return typeof(window.speechSynthesis) !== 'undefined' && !/samsungbrowser/i.test(navigator.userAgent);
16
+ return typeof(window.speechSynthesis) !== 'undefined';
17
17
  }
18
18
 
19
19
  /** @param {TTSEngineOptions} options */
@@ -48,6 +48,10 @@ export default class WebTTSEngine extends AbstractTTSEngine {
48
48
  ],
49
49
  });
50
50
 
51
+ navigator.mediaSession.setPositionState({
52
+ duration: Infinity,
53
+ });
54
+
51
55
  navigator.mediaSession.setActionHandler('play', () => {
52
56
  audio.play();
53
57
  this.resume();
@@ -147,12 +151,12 @@ export class WebTTSSound {
147
151
  this.utterance.rate = this.rate;
148
152
 
149
153
  // Useful for debugging things
150
- if (location.toString().indexOf('_debugReadAloud=true') != -1) {
154
+ if (DEBUG_READ_ALOUD) {
151
155
  this.utterance.addEventListener('pause', () => console.log('pause'));
152
156
  this.utterance.addEventListener('resume', () => console.log('resume'));
153
157
  this.utterance.addEventListener('start', () => console.log('start'));
154
158
  this.utterance.addEventListener('end', () => console.log('end'));
155
- this.utterance.addEventListener('error', () => console.log('error'));
159
+ this.utterance.addEventListener('error', ev => console.log('error', ev));
156
160
  this.utterance.addEventListener('boundary', () => console.log('boundary'));
157
161
  this.utterance.addEventListener('mark', () => console.log('mark'));
158
162
  this.utterance.addEventListener('finish', () => console.log('finish'));
@@ -260,24 +264,25 @@ export class WebTTSSound {
260
264
  // 2. Pause doesn't work and doesn't fire
261
265
  // 3. Pause works but doesn't fire
262
266
  const pauseMightNotWork = (isFirefox() && isAndroid());
263
- const pauseMightNotFire = isChrome() || pauseMightNotWork;
264
267
 
265
- if (pauseMightNotFire) {
266
- // wait for it just in case
267
- const timeoutPromise = sleep(100).then(() => 'timeout');
268
- const result = await Promise.race([pausePromise, timeoutPromise]);
269
- // We got our pause event; nothing to do!
270
- if (result != 'timeout') return;
268
+ // Pause sometimes works, but doesn't fire the event, so wait to see if it fires
269
+ const winner = await Promise.race([pausePromise, sleep(100).then(() => 'timeout')]);
271
270
 
272
- this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
271
+ // We got our pause event; nothing to do!
272
+ if (winner != 'timeout') return;
273
273
 
274
- // if pause might not work, then we'll stop entirely and restart later
275
- if (pauseMightNotWork) this.stop();
274
+ if (DEBUG_READ_ALOUD) {
275
+ console.log('TTS: Firing pause event manually');
276
276
  }
277
+
278
+ this.utterance.dispatchEvent(new CustomEvent('pause', this._lastEvents.start));
279
+
280
+ // if pause might not work, then we'll stop entirely and restart later
281
+ if (pauseMightNotWork) this.stop();
277
282
  }
278
283
 
279
284
  async resume() {
280
- if (!this.started) {
285
+ if (!this.started || this.stopped) {
281
286
  this.play();
282
287
  return;
283
288
  }
@@ -289,22 +294,24 @@ export class WebTTSSound {
289
294
  // 2. Resume works + doesn't fire (Chrome Desktop)
290
295
  // 3. Resume doesn't work + doesn't fire (Chrome/FF Android)
291
296
  const resumeMightNotWork = (isChrome() && isAndroid()) || (isFirefox() && isAndroid());
292
- const resumeMightNotFire = isChrome() || resumeMightNotWork;
293
297
 
294
298
  // Try resume
295
299
  const resumePromise = promisifyEvent(this.utterance, 'resume');
296
300
  speechSynthesis.resume();
297
301
 
298
- if (resumeMightNotFire) {
299
- const result = await Promise.race([resumePromise, sleep(100).then(() => 'timeout')]);
302
+ const winner = await Promise.race([resumePromise, sleep(100).then(() => 'timeout')]);
303
+ // We got resume! All is good
304
+ if (winner != 'timeout') return;
300
305
 
301
- if (result != 'timeout') return;
306
+ if (DEBUG_READ_ALOUD) {
307
+ console.log('TTS: Firing resume event manually');
308
+ }
302
309
 
303
- this.utterance.dispatchEvent(new CustomEvent('resume', {}));
304
- if (resumeMightNotWork) {
305
- await this.reload();
306
- this.play();
307
- }
310
+ // Fake it
311
+ this.utterance.dispatchEvent(new CustomEvent('resume', {}));
312
+ if (resumeMightNotWork) {
313
+ await this.reload();
314
+ this.play();
308
315
  }
309
316
  }
310
317
 
@@ -326,37 +333,56 @@ export class WebTTSSound {
326
333
  * by pausing after 14 seconds and ~instantly resuming.
327
334
  */
328
335
  async _chromePausingBugFix() {
329
- const timeoutPromise = sleep(14000).then(() => 'timeout');
330
- const pausePromise = promisifyEvent(this.utterance, 'pause').then(() => 'paused');
331
- const endPromise = promisifyEvent(this.utterance, 'end').then(() => 'ended');
332
- const result = await Promise.race([timeoutPromise, pausePromise, endPromise]);
333
- if (location.toString().indexOf('_debugReadAloud=true') != -1) {
336
+ if (DEBUG_READ_ALOUD) {
337
+ console.log('CHROME-PAUSE-HACK: starting');
338
+ }
339
+
340
+ const result = await Promise.race([
341
+ sleep(14000).then(() => 'timeout'),
342
+ promisifyEvent(this.utterance, 'pause').then(() => 'pause'),
343
+ promisifyEvent(this.utterance, 'end').then(() => 'end'),
344
+ // Some browsers (Edge) trigger error when the utterance is interrupted/stopped
345
+ promisifyEvent(this.utterance, 'error').then(() => 'error'),
346
+ ]);
347
+
348
+ if (DEBUG_READ_ALOUD) {
334
349
  console.log(`CHROME-PAUSE-HACK: ${result}`);
335
350
  }
336
- switch (result) {
337
- case 'ended':
351
+ if (result == 'end' || result == 'error') {
338
352
  // audio was stopped/finished; nothing to do
339
- break;
340
- case 'paused':
353
+ if (DEBUG_READ_ALOUD) {
354
+ console.log('CHROME-PAUSE-HACK: stopped (end/error)');
355
+ }
356
+ } else if (result == 'pause') {
341
357
  // audio was paused; wait for resume
342
358
  // Chrome won't let you resume the audio if 14s have passed 🤷‍
343
359
  // We could do the same as before (but resume+pause instead of pause+resume),
344
360
  // but that means we'd _constantly_ be running in the background. So in that
345
361
  // case, let's just restart the chunk
346
- await Promise.race([
347
- promisifyEvent(this.utterance, 'resume'),
362
+ const result2 = await Promise.race([
363
+ promisifyEvent(this.utterance, 'resume').then(() => 'resume'),
348
364
  sleep(14000).then(() => 'timeout'),
349
365
  ]);
350
- result == 'timeout' ? this.reload() : this._chromePausingBugFix();
351
- break;
352
- case 'timeout':
353
- // We hit Chrome's secret cut off time. Pause/resume
354
- // to be able to keep TTS-ing
366
+ if (result2 == 'timeout') {
367
+ if (DEBUG_READ_ALOUD) {
368
+ console.log('CHROME-PAUSE-HACK: stopped (timed out while paused)');
369
+ }
370
+ // We hit Chrome's secret cut off time while paused, and
371
+ // won't be able to resume normally, so trigger a stop.
372
+ this.stop();
373
+ } else {
374
+ // The user resumed before the cut off! Continue as normal
375
+ this._chromePausingBugFix();
376
+ }
377
+ } else if (result == 'timeout') {
378
+ // We hit Chrome's secret cut off time while playing.
379
+ // To be able to keep TTS-ing, quickly pause/resume.
355
380
  speechSynthesis.pause();
356
381
  await sleep(25);
357
382
  speechSynthesis.resume();
383
+
384
+ // Listen for more
358
385
  this._chromePausingBugFix();
359
- break;
360
386
  }
361
387
  }
362
388
  }
@@ -278,10 +278,8 @@ export class TtsPlugin extends BookReaderPlugin {
278
278
  * @param {Number} leafIndex
279
279
  */
280
280
  async maybeFlipToIndex(leafIndex) {
281
- if (this.br.constMode2up != this.br.mode) {
281
+ if (!this.br._isIndexDisplayed(leafIndex)) {
282
282
  this.br.jumpToIndex(leafIndex);
283
- } else {
284
- await this.br._modes.mode2Up.mode2UpLit.jumpToIndex(leafIndex);
285
283
  }
286
284
  }
287
285
 
@@ -79,3 +79,16 @@ export function hasLocalStorage() {
79
79
  return false;
80
80
  }
81
81
  }
82
+
83
+ export const DEBUG_READ_ALOUD = location.toString().indexOf('_debugReadAloud=true') != -1;
84
+
85
+ export async function checkIfFiresPause() {
86
+ // Pick some random text so that if it accidentally speaks, it's not too annoying
87
+ const u = new SpeechSynthesisUtterance("Loading");
88
+ let calledPause = false;
89
+ u.addEventListener('pause', () => calledPause = true);
90
+ speechSynthesis.speak(u);
91
+ await new Promise(res => setTimeout(res, 10));
92
+ speechSynthesis.pause();
93
+ return calledPause;
94
+ }
@@ -7,7 +7,17 @@
7
7
  * @return {boolean}
8
8
  */
9
9
  export function isChrome(userAgent = navigator.userAgent, vendor = navigator.vendor) {
10
- return /chrome/i.test(userAgent) && /google inc/i.test(vendor);
10
+ return /chrome/i.test(userAgent) && /google inc/i.test(vendor) && !isEdge(userAgent);
11
+ }
12
+
13
+ /**
14
+ * Checks whether the current browser is a Edge browser
15
+ * See https://learn.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance
16
+ * @param {string} [userAgent]
17
+ * @return {boolean}
18
+ */
19
+ export function isEdge(userAgent = navigator.userAgent) {
20
+ return /chrome/i.test(userAgent) && /\bEdg(e|A|iOS)?\b/i.test(userAgent);
11
21
  }
12
22
 
13
23
  /**
@@ -2,7 +2,7 @@ export const TEST_TEXT_FOUND = 'theory';
2
2
  export const TEST_TEXT_NOT_FOUND = 'HopefullyNotFoundLongWord';
3
3
  export const PAGE_FIRST_RESULT = 30;
4
4
 
5
- export const SEARCH_INSIDE_URL_RE = /https:\/\/ia[0-9]+\.us\.archive\.org\/fulltext\/inside\.php\?item_id=.*/;
5
+ export const SEARCH_INSIDE_URL_RE = /\/fulltext\/inside\.php/;
6
6
 
7
7
  /** Mock response for a matching search term. */
8
8
  export function mockResponseFound(req, res) {
@@ -164,14 +164,20 @@ describe('<book-navigator>', () => {
164
164
  expect(el.menuProviders.visualAdjustments).toBeInstanceOf(VisualAdjustmentsProvider);
165
165
  });
166
166
  describe('Loading Sub Menus By Plugin Flags', () => {
167
- test('Search: uses `enableSearch` flag', async() => {
167
+ test('Search: uses `enabled` flag', async() => {
168
168
  const el = fixtureSync(container());
169
169
  const $brContainer = document.createElement('div');
170
170
  const brStub = {
171
171
  resize: sinon.fake(),
172
172
  currentIndex: sinon.fake(),
173
173
  jumpToIndex: sinon.fake(),
174
- options: { enableSearch: true },
174
+ options: {
175
+ plugins: {
176
+ search: {
177
+ enabled: true,
178
+ },
179
+ },
180
+ },
175
181
  refs: {
176
182
  $brContainer,
177
183
  },
@@ -346,7 +352,6 @@ describe('<book-navigator>', () => {
346
352
 
347
353
  let sidePanelConfig = {};
348
354
  el.addEventListener('updateSideMenu', (e) => {
349
- console.log();
350
355
  sidePanelConfig = e.detail;
351
356
  });
352
357
  const toggleSearchMenuEvent = new Event('BookReader:ToggleSearchMenu');
@@ -88,7 +88,11 @@ describe('Search Provider', () => {
88
88
  onProviderChange: sinon.fake(),
89
89
  bookreader: {
90
90
  leafNumToIndex: sinon.fake(),
91
- _searchPluginGoToResult: sinon.fake(),
91
+ _plugins: {
92
+ search: {
93
+ jumpToMatch: sinon.fake(),
94
+ },
95
+ },
92
96
  },
93
97
  });
94
98
 
@@ -100,7 +104,7 @@ describe('Search Provider', () => {
100
104
  { detail: searchResultStub }),
101
105
  );
102
106
 
103
- expect(provider.bookreader._searchPluginGoToResult.callCount).toEqual(1);
107
+ expect(provider.bookreader._plugins.search.jumpToMatch.callCount).toEqual(1);
104
108
  });
105
109
  test('update url when search is cancelled or input cleared', async() => {
106
110
  const urlPluginMock = {
@@ -111,7 +115,11 @@ describe('Search Provider', () => {
111
115
  onProviderChange: sinon.fake(),
112
116
  bookreader: {
113
117
  leafNumToIndex: sinon.fake(),
114
- _searchPluginGoToResult: sinon.fake(),
118
+ _plugins: {
119
+ search: {
120
+ jumpToMatch: sinon.fake(),
121
+ },
122
+ },
115
123
  urlPlugin: urlPluginMock,
116
124
  },
117
125
  });
@@ -145,7 +153,11 @@ describe('Search Provider', () => {
145
153
  onProviderChange: sinon.fake(),
146
154
  bookreader: {
147
155
  leafNumToIndex: sinon.fake(),
148
- _searchPluginGoToResult: sinon.fake(),
156
+ _plugins: {
157
+ search: {
158
+ jumpToMatch: sinon.fake(),
159
+ },
160
+ },
149
161
  urlPlugin: urlPluginMock,
150
162
  search: sinon.fake(),
151
163
  },
@@ -39,7 +39,7 @@ describe('Sharing Provider', () => {
39
39
  item,
40
40
  baseHost,
41
41
  bookreader: {
42
- options: { subPrefix },
42
+ subPrefix,
43
43
  },
44
44
  });
45
45
 
@@ -191,3 +191,73 @@ describe('`BookReader.prototype.prev`', () => {
191
191
  });
192
192
  });
193
193
 
194
+ describe('`BookReader.prototype.jumpToIndex`', () => {
195
+ /**
196
+ * @param {Partial<BookReaderOptions>} overrides
197
+ */
198
+ function makeFakeBr(overrides = {}) {
199
+ const br = new BookReader({
200
+ data: [
201
+ [
202
+ { index: 0, viewable: true },
203
+ ],
204
+ [
205
+ { index: 1, viewable: false },
206
+ { index: 2, viewable: false },
207
+ ],
208
+ [
209
+ { index: 3, viewable: false },
210
+ { index: 4, viewable: false },
211
+ ],
212
+ [
213
+ { index: 5, viewable: true },
214
+ ],
215
+ ],
216
+ ...overrides,
217
+ });
218
+ br.init();
219
+
220
+ br._modes.mode2Up.jumpToIndex = sinon.fake();
221
+
222
+ expect(br.firstIndex).toBe(0);
223
+ expect(br.mode).toBe(br.constMode2up);
224
+
225
+ return br;
226
+ }
227
+
228
+ test('Jumping into an unviewables range will go to start of range', () => {
229
+ const br = makeFakeBr();
230
+ br.jumpToIndex(3, 0, 0, true);
231
+ expect(br._modes.mode2Up.jumpToIndex.callCount).toBe(1);
232
+ expect(br._modes.mode2Up.jumpToIndex.args[0][0]).toBe(1);
233
+ });
234
+
235
+ test('Trying to jump into unviewables range while in that range, will jump forward', () => {
236
+ const br = makeFakeBr();
237
+ br.displayedIndices = [1, 2];
238
+ br.jumpToIndex(3, 0, 0, true);
239
+ expect(br._modes.mode2Up.jumpToIndex.callCount).toBe(1);
240
+ expect(br._modes.mode2Up.jumpToIndex.args[0][0]).toBe(5);
241
+ });
242
+
243
+ test('Trying to jump into unviewables range while in that range, will do nothing if cannot jump forward', () => {
244
+ const br = makeFakeBr({
245
+ data: [
246
+ [
247
+ { index: 0, viewable: true },
248
+ ],
249
+ [
250
+ { index: 1, viewable: false },
251
+ { index: 2, viewable: false },
252
+ ],
253
+ [
254
+ { index: 3, viewable: false },
255
+ { index: 4, viewable: false },
256
+ ],
257
+ ],
258
+ });
259
+ br.displayedIndices = [1, 2];
260
+ br.jumpToIndex(3, 0, 0, true);
261
+ expect(br._modes.mode2Up.jumpToIndex.callCount).toBe(0);
262
+ });
263
+ });
@@ -7,7 +7,12 @@ import '@/src/plugins/url/plugin.url.js';
7
7
  let br;
8
8
  beforeAll(() => {
9
9
  document.body.innerHTML = '<div id="BookReader">';
10
- br = new BookReader();
10
+ br = new BookReader({
11
+ server: '',
12
+ bookId: '',
13
+ subPrefix: '',
14
+ bookPath: '',
15
+ });
11
16
  });
12
17
 
13
18
  afterEach(() => {
@@ -91,6 +96,26 @@ test('calls switchMode with init option when init called', () => {
91
96
  .toHaveProperty('init', true);
92
97
  });
93
98
 
99
+ test('has added BR property: server', () => {
100
+ expect(br).toHaveProperty('server');
101
+ expect(br.server).toBeDefined();
102
+ });
103
+
104
+ test('has added BR property: bookId', () => {
105
+ expect(br).toHaveProperty('bookId');
106
+ expect(br.bookId).toBeDefined();
107
+ });
108
+
109
+ test('has added BR property: subPrefix', () => {
110
+ expect(br).toHaveProperty('subPrefix');
111
+ expect(br.subPrefix).toBeDefined();
112
+ });
113
+
114
+ test('has added BR property: bookPath', () => {
115
+ expect(br).toHaveProperty('bookPath');
116
+ expect(br.bookPath).toBeDefined();
117
+ });
118
+
94
119
  test('has suppressFragmentChange true when init with no input', () => {
95
120
  br._plugins.resume.getResumeValue = jest.fn(() => null);
96
121
  br.urlReadFragment = jest.fn(() => '');