@internetarchive/ia-book-actions 0.0.29 → 0.1.1

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.
@@ -1,17 +1,22 @@
1
1
  /* eslint-disable camelcase */
2
- import { html, css, LitElement } from 'lit';
2
+ import { html, css, LitElement, nothing } from 'lit';
3
3
 
4
4
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
5
5
  import { ModalConfig } from '@internetarchive/modal-manager';
6
+ import { ToastConfig } from '@internetarchive/toast-manager';
7
+ import { LocalCache } from '@internetarchive/local-cache';
6
8
 
7
9
  import './components/collapsible-action-group.js';
8
10
  import './components/book-title-bar.js';
9
11
  import './components/text-group.js';
10
12
  import './components/info-icon.js';
13
+ import './components/timer-countdown.js';
11
14
 
12
15
  import { GetLendingActions } from './core/services/get-lending-actions.js';
13
16
  import { mobileContainerWidth } from './core/config/constants.js';
17
+ import { sentryLogs } from './core/config/sentry-events.js';
14
18
  import { LoanTokenPoller } from './core/services/loan-token-poller.js';
19
+ import { LoanRenewHelper } from './core/services/loan-renew-helper.js';
15
20
 
16
21
  export const events = {
17
22
  browseExpired: 'IABookReader:BrowsingHasExpired',
@@ -36,6 +41,9 @@ export default class IABookActions extends LitElement {
36
41
  disableActionGroup: { type: Boolean },
37
42
  modal: { Object },
38
43
  tokenDelay: { type: Number },
44
+ localCache: { type: Object },
45
+ loanRenewTimeConfig: { type: Object },
46
+ loanRenewResult: { type: Object },
39
47
  };
40
48
  }
41
49
 
@@ -53,7 +61,7 @@ export default class IABookActions extends LitElement {
53
61
  this.sharedObserver = undefined;
54
62
  this.disableActionGroup = false;
55
63
  this.modal = undefined;
56
- this.tokenDelay = 120000; // 2 minutes
64
+ this.tokenDelay = 120; // in seconds
57
65
 
58
66
  // private props
59
67
  this.primaryActions = [];
@@ -63,15 +71,51 @@ export default class IABookActions extends LitElement {
63
71
  this.lendingOptions = {};
64
72
  this.borrowType = ''; // (browsed|borrowed)
65
73
  this.consecutiveLoanCounts = 1; // consecutive loan count
74
+ this.suppressToast = false;
75
+ this.browseTimer = undefined;
76
+
77
+ /**
78
+ * contains one hour auto-loan-renew time configuration
79
+ *
80
+ * @type {object} loanRenewTimeConfig
81
+ * @property {number} loanTotalTime - total seconds a loan does have
82
+ * @property {number} loanRenewAtLast - check for loan renew at last
83
+ * @property {number} pageChangedInLast - consider loan renew eligible if viewed new page
84
+ */
85
+ this.loanRenewTimeConfig = {};
86
+
87
+ /**
88
+ * contains one hour auto-loan-renew response
89
+ *
90
+ * @type {object} loanRenewResult
91
+ * @property {string} texts - texts messages need to show in toast message
92
+ * @property {boolean} renewNow - key to determine if need to renew now
93
+ * @property {number} secondsLeft - seconds left in active loan
94
+ */
95
+ this.loanRenewResult = { texts: '', renewNow: false, secondsLeft: 0 };
96
+
97
+ /**
98
+ * need showdowRoot opened for resetTimerCountState
99
+ * @see IABookActions::resetTimerCountState
100
+ */
101
+ this._shadowRoot = this.attachShadow({ mode: 'open' });
66
102
  }
67
103
 
68
104
  disconnectedCallback() {
69
105
  this.tokenPoller?.disconnectedInterval();
70
- window?.Sentry?.captureMessage('disconnectedCallback');
106
+ window?.Sentry?.captureMessage(sentryLogs.disconnectedCallback);
71
107
  this.disconnectResizeObserver();
72
108
  }
73
109
 
74
110
  firstUpdated() {
111
+ // bind auto loan renew events
112
+ this.bindLoanRenewEvents();
113
+
114
+ // localCache used for auto-loan-renew
115
+ this.localCache = new LocalCache({
116
+ namespace: 'loanRenew',
117
+ });
118
+
75
119
  if (!this.sharedObserver) {
76
120
  this.sharedObserver = new SharedResizeObserver();
77
121
  this.setupResizeObserver();
@@ -90,27 +134,6 @@ export default class IABookActions extends LitElement {
90
134
  }
91
135
  }
92
136
 
93
- browseHasExpired() {
94
- const currStatus = { ...this.lendingStatus, browsingExpired: true };
95
- this.lendingStatus = currStatus;
96
- }
97
-
98
- startBrowseTimer() {
99
- const {
100
- browsingExpired,
101
- user_has_browsed,
102
- secondsLeftOnLoan,
103
- } = this.lendingStatus;
104
- if (!user_has_browsed || browsingExpired) {
105
- return;
106
- }
107
-
108
- const timeLeft = secondsLeftOnLoan * 1000;
109
- setTimeout(() => {
110
- this.browseHasExpired();
111
- }, timeLeft);
112
- }
113
-
114
137
  /** SharedObserver resize handler */
115
138
  handleResize(entry) {
116
139
  // if you are observing multiple targets,
@@ -144,6 +167,8 @@ export default class IABookActions extends LitElement {
144
167
  /** End SharedObserver resize handler */
145
168
 
146
169
  async setupLendingToolbarActions() {
170
+ this.suppressToast = false;
171
+
147
172
  this.lendingOptions = new GetLendingActions(
148
173
  this.userid,
149
174
  this.identifier,
@@ -169,10 +194,8 @@ export default class IABookActions extends LitElement {
169
194
  'browsingExpired' in this.lendingStatus &&
170
195
  this.lendingStatus?.browsingExpired;
171
196
  if (hasExpired) {
172
- if (!this.tokenPoller.disconnectedInterval) {
173
- window?.Sentry?.captureMessage(
174
- 'setupLendingToolbarActions hasExpired - no tokenPoller'
175
- );
197
+ if (!this.tokenPoller) {
198
+ window?.Sentry?.captureMessage(sentryLogs.bookWasExpired);
176
199
  }
177
200
  this.tokenPoller?.disconnectedInterval();
178
201
  /** Global event - always fire */
@@ -183,12 +206,13 @@ export default class IABookActions extends LitElement {
183
206
  composed: true,
184
207
  })
185
208
  );
209
+
210
+ // early return if book is already expired
186
211
  return;
187
212
  }
188
213
 
189
214
  if (this.borrowType === 'browsed') {
190
215
  // start timer for browsed.
191
- // when browse is completed, we shows browse-again button
192
216
  this.startBrowseTimer();
193
217
  }
194
218
 
@@ -198,7 +222,214 @@ export default class IABookActions extends LitElement {
198
222
  return;
199
223
  }
200
224
 
201
- this.startLoanTokenPoller();
225
+ // initial timer state for 1-hour loan
226
+ if (this.borrowType === 'browsed' && !this.loanRenewResult.renewNow) {
227
+ setTimeout(() => {
228
+ this.resetTimerCountState();
229
+ }, 100);
230
+ }
231
+
232
+ /**
233
+ * tokenPoller determines if user has loan token for this book
234
+ * - if book is going to renew, need to wait until renew is completed
235
+ * - otherwise execute immediately
236
+ */
237
+ setTimeout(
238
+ () => {
239
+ this.startLoanTokenPoller();
240
+ },
241
+
242
+ // if book is renew while reading, let's wait to execute create-token api
243
+ this.loanRenewResult.renewNow ? this.tokenDelay * 1000 : 100
244
+ );
245
+ }
246
+
247
+ /**
248
+ * Bind 1 hour loan auto renew event,
249
+ * There are two events we want to use,
250
+ * 1. BookReader:userAction - dispatched from bookreader side
251
+ * 2. click event to refresh the page when book is auto expired
252
+ * 3. IABookActions:loanRenew - dispatched from timer-countdown component
253
+ */
254
+ bindLoanRenewEvents() {
255
+ /**
256
+ * dispatched this event from bookreader page changed
257
+ */
258
+ window.addEventListener('BookReader:userAction', () => {
259
+ if (this.borrowType === 'browsed') {
260
+ this.suppressToast = false;
261
+ this.autoLoanRenewChecker(true);
262
+ }
263
+
264
+ return nothing;
265
+ });
266
+
267
+ /**
268
+ * detect click-event on document to close toast-template
269
+ */
270
+ document.addEventListener('click', e => {
271
+ if (this.loanRenewHelper && this.loanRenewResult.secondsLeft > 0) {
272
+ this.suppressToast = true;
273
+ }
274
+
275
+ // reload when user click on book-page and book is EXPIRED
276
+ const hasExpired =
277
+ 'browsingExpired' in this.lendingStatus &&
278
+ this.lendingStatus?.browsingExpired;
279
+ if (hasExpired && e.target.nodeName !== 'IA-BOOK-ACTIONS') {
280
+ window.location?.reload();
281
+ }
282
+ });
283
+
284
+ /**
285
+ * this event dispatched from timer-countdown component for:
286
+ * 1. user turned book page after showing auto-returned warning message
287
+ * 2. show warning message having remaining time
288
+ * @see TimerCountdown::timerCountdown
289
+ */
290
+ this.addEventListener('IABookActions:loanRenew', async event => {
291
+ await this.autoLoanRenewChecker(false);
292
+
293
+ // show warning message with remaining time to auto returned it.
294
+ if (this.loanRenewResult.renewNow === false) {
295
+ this.loanRenewResult.secondsLeft = event.detail.secondsLeft;
296
+ this.showToastMessage();
297
+ }
298
+ });
299
+ }
300
+
301
+ /**
302
+ * To determine if need to be renewed browsed book
303
+ * @see LoanRenewHelper
304
+ *
305
+ * @param {Boolean} hasPageChanged
306
+ */
307
+ async autoLoanRenewChecker(hasPageChanged = false) {
308
+ this.loanRenewHelper = new LoanRenewHelper(
309
+ hasPageChanged,
310
+ this.identifier,
311
+ this.localCache,
312
+ this.loanRenewTimeConfig
313
+ );
314
+
315
+ await this.loanRenewHelper.handleLoanRenew();
316
+ this.loanRenewResult = this.loanRenewHelper.result;
317
+ }
318
+
319
+ /**
320
+ * Show toast messages on some specific loan renew features. e.g.
321
+ * - show message when book is auto renewed
322
+ * - show message when book is expired
323
+ * - show message having remaining time when book is about to auto returned
324
+ */
325
+ async showToastMessage() {
326
+ if (this.suppressToast) return;
327
+
328
+ const iaBookActions = document.querySelector('ia-book-actions')?.shadowRoot;
329
+ if (!iaBookActions) return;
330
+
331
+ let toastTemplate = iaBookActions.querySelector('toast-template');
332
+ if (!toastTemplate) {
333
+ toastTemplate = document.createElement('toast-template');
334
+ }
335
+ await iaBookActions.appendChild(toastTemplate);
336
+
337
+ // if secondsLeft < 60, consider it 1 minute
338
+ let { secondsLeft } = this.loanRenewResult;
339
+ secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
340
+
341
+ const config = new ToastConfig();
342
+ config.dismisOnClick = true;
343
+ config.texts = this.loanRenewHelper?.getMessageTexts(
344
+ this.loanRenewResult.texts,
345
+ secondsLeft
346
+ );
347
+
348
+ toastTemplate.showToast({
349
+ config,
350
+ });
351
+ }
352
+
353
+ async browseHasRenew() {
354
+ const loanTime = await this.localCache.get(`${this.identifier}-loanTime`);
355
+ const secondsLeft = (loanTime - new Date()) / 1000; // different in seconds
356
+
357
+ const currStatus = {
358
+ ...this.lendingStatus,
359
+ user_has_browsed: true,
360
+ browsingExpired: false,
361
+ secondsLeftOnLoan: secondsLeft,
362
+ };
363
+ this.lendingStatus = currStatus;
364
+
365
+ this.startBrowseTimer();
366
+ }
367
+
368
+ startBrowseTimer() {
369
+ clearTimeout(this.browseTimer);
370
+ const {
371
+ browsingExpired,
372
+ user_has_browsed,
373
+ secondsLeftOnLoan,
374
+ } = this.lendingStatus;
375
+ if (!user_has_browsed || browsingExpired) {
376
+ return;
377
+ }
378
+
379
+ this.browseTimer = setTimeout(() => {
380
+ this.browseHasExpired();
381
+ }, secondsLeftOnLoan * 1000);
382
+ }
383
+
384
+ /**
385
+ * Execute when loan is expired
386
+ */
387
+ async browseHasExpired() {
388
+ this.tokenPoller?.disconnectedInterval();
389
+
390
+ const currStatus = { ...this.lendingStatus, browsingExpired: true };
391
+ this.lendingStatus = currStatus;
392
+
393
+ // remove respected key:value for loan-renew
394
+ await this.localCache.delete(`${this.identifier}-loanTime`);
395
+ await this.localCache.delete(`${this.identifier}-pageChangedTime`);
396
+
397
+ // show message after browsed book is expired.
398
+ this.loanRenewResult.renewNow = false;
399
+ this.loanRenewResult.texts =
400
+ 'This book has been automatically returned due to inactivity.';
401
+
402
+ this.suppressToast = false;
403
+ this.showToastMessage();
404
+
405
+ window?.Sentry?.captureMessage(sentryLogs.browseHasExpired);
406
+ }
407
+
408
+ /**
409
+ * Reset timer animation state after book renewed
410
+ */
411
+ async resetTimerCountState() {
412
+ const timerCountdown = this._shadowRoot.querySelector('timer-countdown');
413
+ if (!timerCountdown) return;
414
+
415
+ const secondsLeft = Number(this.lendingStatus.secondsLeftOnLoan);
416
+ const strokeDashOffset =
417
+ (secondsLeft / this.loanRenewTimeConfig.loanTotalTime) * 315;
418
+
419
+ // set seconds left in loan expire
420
+ timerCountdown.style?.setProperty('--secondsLeftOnLoan', `${secondsLeft}s`);
421
+
422
+ // set circle stroke offset left in loan expire
423
+ timerCountdown.style?.setProperty(
424
+ '--strokeLeftOnLoan',
425
+ `${Number(strokeDashOffset)}` // the perimeter of the circle = (π * 2 * radius)
426
+ );
427
+
428
+ const animationCircle = timerCountdown.shadowRoot.querySelector('.circle');
429
+ animationCircle.style.animationName = 'none';
430
+ setTimeout(() => {
431
+ animationCircle.style.animationName = 'circletimer';
432
+ }, 100);
202
433
  }
203
434
 
204
435
  render() {
@@ -220,6 +451,16 @@ export default class IABookActions extends LitElement {
220
451
  ></book-title-bar>`;
221
452
  }
222
453
 
454
+ get timerCountdownTemplate() {
455
+ return this.borrowType === 'browsed'
456
+ ? html`<timer-countdown
457
+ .secondsLeftOnLoan=${Number(this.lendingStatus.secondsLeftOnLoan)}
458
+ .loanTotalTime=${this.loanRenewTimeConfig.loanTotalTime}
459
+ .loanRenewAtLast=${this.loanRenewTimeConfig.loanRenewAtLast}
460
+ ></timer-countdown>`
461
+ : nothing;
462
+ }
463
+
223
464
  get bookActionBar() {
224
465
  return html`
225
466
  <collapsible-action-group
@@ -231,23 +472,56 @@ export default class IABookActions extends LitElement {
231
472
  .width=${this.width}
232
473
  .borrowType=${this.borrowType}
233
474
  .returnUrl=${this.returnUrl}
475
+ .localCache=${this.localCache}
476
+ .loanTotalTime=${this.loanRenewTimeConfig.loanTotalTime}
234
477
  ?hasAdminAccess=${this.hasAdminAccess}
235
478
  ?disabled=${this.disableActionGroup}
479
+ ?renewNow=${this.loanRenewResult.renewNow}
480
+ @loanAutoRenewed=${this.handleLoanAutoRenewed}
236
481
  @lendingActionError=${this.handleLendingActionError}
237
482
  @toggleActionGroup=${this.handleToggleActionGroup}
238
483
  >
239
484
  </collapsible-action-group>
240
485
  ${this.textGroupTemplate} ${this.infoIconTemplate}
486
+ ${this.timerCountdownTemplate}
241
487
  `;
242
488
  }
243
489
 
490
+ /**
491
+ * Execute after auto loan renewed is completed
492
+ * - show success message
493
+ * - then change the remaining time
494
+ * - reset timer state
495
+ */
496
+ async handleLoanAutoRenewed() {
497
+ if (this.loanRenewResult.renewNow) {
498
+ this.tokenPoller?.disconnectedInterval();
499
+
500
+ this.suppressToast = false;
501
+ await this.showToastMessage();
502
+ await this.browseHasRenew();
503
+ await this.resetTimerCountState();
504
+
505
+ window?.Sentry?.captureMessage(`${sentryLogs.bookHasRenewed}`);
506
+ // TODO: analytics?? for consecutive renew.....
507
+
508
+ // testing console....
509
+ console.log(sentryLogs.bookHasRenewed, {
510
+ loanRenewResult: this.loanRenewResult,
511
+ browseHasRenew: this.lendingStatus.secondsLeftOnLoan,
512
+ resetTimerCountState: this._shadowRoot.querySelector('timer-countdown'),
513
+ });
514
+ }
515
+ }
516
+
244
517
  /**
245
518
  * enable access of borrowed/browsed books
519
+ * @see LoanTokenPoller
246
520
  */
247
521
  startLoanTokenPoller() {
248
522
  if (this.tokenPoller) {
249
- window?.Sentry?.captureMessage('startLoanTokenPoller clearing interval');
250
- this.tokenPoller.disconnectedInterval();
523
+ window?.Sentry?.captureMessage(sentryLogs.clearTokenPoller);
524
+ this.tokenPoller?.disconnectedInterval();
251
525
  }
252
526
  const successCallback = () => {
253
527
  this.lendingBarPostInit();
@@ -272,7 +546,7 @@ export default class IABookActions extends LitElement {
272
546
  this.borrowType,
273
547
  successCallback,
274
548
  errorCallback,
275
- this.tokenDelay // 1000 ms = 1 sec
549
+ this.tokenDelay // in seconds
276
550
  );
277
551
  }
278
552
 
@@ -303,6 +577,20 @@ export default class IABookActions extends LitElement {
303
577
 
304
578
  if (errorMsg) this.showErrorModal(errorMsg, action);
305
579
 
580
+ // if error related to loan token
581
+ // - clear tokenPoller interval
582
+ // - set user_has_browsed to `false`
583
+ if (action === 'create_token') {
584
+ this.tokenPoller?.disconnectedInterval();
585
+
586
+ const currStatus = {
587
+ ...this.lendingStatus,
588
+ user_has_browsed: false,
589
+ available_to_browse: true,
590
+ };
591
+ this.lendingStatus = currStatus;
592
+ }
593
+
306
594
  // update action bar state if book is not available to browse or borrow.
307
595
  if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
308
596
  let currStatus = this.lendingStatus;
@@ -358,7 +646,8 @@ export default class IABookActions extends LitElement {
358
646
  href="mailto:info@archive.org?subject=Help: cannot access my borrowed book: ${this
359
647
  .identifier}"
360
648
  >info@archive.org</a
361
- >`;
649
+ ><br /><br />
650
+ <code>errorLog: ${errorMsg}</code>`;
362
651
  }
363
652
 
364
653
  this.modal.showModal({
@@ -379,11 +668,13 @@ export default class IABookActions extends LitElement {
379
668
  }
380
669
 
381
670
  get textGroupTemplate() {
382
- return html`<text-group
383
- textClass=${this.textClass}
384
- texts=${this.primaryTitle}
385
- >
386
- </text-group>`;
671
+ return this.primaryTitle
672
+ ? html`<text-group
673
+ textClass=${this.textClass}
674
+ .texts=${this.primaryTitle}
675
+ >
676
+ </text-group>`
677
+ : nothing;
387
678
  }
388
679
 
389
680
  get hasAdminAccess() {
@@ -394,17 +685,24 @@ export default class IABookActions extends LitElement {
394
685
  return css`
395
686
  :host {
396
687
  display: block;
397
- background: var(--primaryBGColor, #000);
398
- color: var(--primaryTextColor, #fff);
399
688
  }
689
+
400
690
  .lending-wrapper {
401
691
  width: 100%;
402
692
  margin: 0 auto;
693
+ background: var(--primaryBGColor, #000);
694
+ color: var(--primaryTextColor, #fff);
403
695
  display: inline-flex;
404
696
  align-items: center;
405
697
  justify-content: center;
406
698
  flex-wrap: wrap;
407
699
  }
700
+
701
+ toast-template {
702
+ --toastTopMargin: 80px;
703
+ --toastBGColor: #333;
704
+ --toastFontColor: #fff;
705
+ }
408
706
  `;
409
707
  }
410
708
  }
@@ -6,7 +6,7 @@ const container = ({
6
6
  {
7
7
  analyticsEvent: { action: 'Browse', category: 'Lending' },
8
8
  className: 'ia-button primary',
9
- text: 'Borrow for 1 hour',
9
+ text: 'Borrow',
10
10
  id: 'browseBook',
11
11
  },
12
12
  {
@@ -40,7 +40,7 @@ describe('<collapsible-action-group>', () => {
40
40
  .true;
41
41
 
42
42
  const primaryButton = primaryActionContainer.querySelector('.ia-button');
43
- expect(primaryButton.innerText).to.equal('Borrow for 1 hour');
43
+ expect(primaryButton.innerText).to.equal('Borrow');
44
44
  });
45
45
 
46
46
  it('check if loader is active and action-group is disabled', async () => {
@@ -0,0 +1,45 @@
1
+ import { html, fixture, expect, aTimeout } from '@open-wc/testing';
2
+ import '../../src/components/timer-countdown.js';
3
+
4
+ const container = ({ secondsLeftOnLoan, loanRenewTimeConfig } = {}) =>
5
+ html`<timer-countdown
6
+ .secondsLeftOnLoan=${secondsLeftOnLoan}
7
+ .loanRenewTimeConfig=${loanRenewTimeConfig}
8
+ ></timer-countdown>`;
9
+
10
+ describe('<timer-countdown>', () => {
11
+ it('timer interval is undefined when loan is expired', async () => {
12
+ const el = await fixture(
13
+ container({
14
+ secondsLeftOnLoan: 0, // in seconds
15
+ loanRenewTimeConfig: {
16
+ loanTotalTime: 14, // in seconds
17
+ loanRenewAtLast: 5, // in seconds
18
+ pageChangedInLast: 5, // in seconds
19
+ },
20
+ })
21
+ );
22
+
23
+ await el.updateComplete;
24
+ expect(el.timerInterval).to.be.undefined;
25
+ });
26
+
27
+ it('timer interval is not undefined when loan is active', async () => {
28
+ const el = await fixture(
29
+ container({
30
+ secondsLeftOnLoan: 2, // in seconds
31
+ loanRenewTimeConfig: {
32
+ loanTotalTime: 14, // in seconds
33
+ loanRenewAtLast: 5, // in seconds
34
+ pageChangedInLast: 5, // in seconds
35
+ },
36
+ })
37
+ );
38
+
39
+ await aTimeout(1900);
40
+ await el.updateComplete;
41
+
42
+ expect(el.secondsLeftOnLoan).to.equal(2);
43
+ expect(el.timerInterval).to.not.be.undefined;
44
+ });
45
+ });
@@ -25,7 +25,7 @@ describe('Get Lending Actions', () => {
25
25
 
26
26
  expect(actions.primaryTitle).to.equal(bookTitles.available_1hr);
27
27
  expect(actions.primaryActions.length).to.equal(3);
28
- expect(actions.primaryActions[0].text).to.equal('Borrow for 1 hour');
28
+ expect(actions.primaryActions[0].text).to.equal('Borrow');
29
29
  expect(actions.primaryActions[1].text).to.equal('Borrow for 14 days');
30
30
  expect(actions.secondaryActions[1].text).to.equal('Purchase at');
31
31
  });
@@ -111,7 +111,7 @@ describe('Get Lending Actions', () => {
111
111
  expect(actions.primaryTitle).to.equal(bookTitles.claim_waitlist);
112
112
  expect(actions.primaryActions.length).to.equal(3);
113
113
  expect(actions.primaryActions[0].text).to.equal('Borrow for 14 days');
114
- expect(actions.primaryActions[1].text).to.equal('Borrow for 1 hour');
114
+ expect(actions.primaryActions[1].text).to.equal('Borrow');
115
115
  expect(actions.primaryActions[2].text).to.equal('Leave Waitlist');
116
116
  expect(spy.called).to.be.true;
117
117
  });
@@ -138,7 +138,7 @@ describe('Get Lending Actions', () => {
138
138
  expect(actions.primaryTitle).to.equal(bookTitles.on_waitlist);
139
139
  expect(actions.primaryActions.length).to.equal(2);
140
140
  expect(actions.primaryActions[0].text).to.equal('Leave Waitlist');
141
- expect(actions.primaryActions[1].text).to.equal('Borrow for 1 hour');
141
+ expect(actions.primaryActions[1].text).to.equal('Borrow');
142
142
  expect(claimSpy.called).to.be.false;
143
143
  expect(waitlistSpy.called).to.be.true;
144
144
  });
@@ -162,7 +162,7 @@ describe('Get Lending Actions', () => {
162
162
  expect(actions.primaryTitle).to.equal(bookTitles.on_waitlist);
163
163
  expect(actions.primaryActions.length).to.equal(2);
164
164
  expect(actions.primaryActions[0].text).to.equal('Leave Waitlist');
165
- expect(actions.primaryActions[1].text).to.equal('Borrow for 1 hour');
165
+ expect(actions.primaryActions[1].text).to.equal('Borrow');
166
166
  expect(waitlistSpy.called).to.be.true;
167
167
  });
168
168
  });