@internetarchive/ia-book-actions 1.4.2 → 1.4.3-canary.0
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/package.json
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
"description": "Bookreader actions for borrowable book",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
|
-
"url": "git+https://github.com/internetarchive/iaux-
|
|
6
|
+
"url": "git+https://github.com/internetarchive/iaux-book-actions.git"
|
|
7
7
|
},
|
|
8
8
|
"author": "Internet Archive",
|
|
9
9
|
"license": "AGPL-3.0-only",
|
|
10
|
-
"version": "1.4.
|
|
10
|
+
"version": "1.4.3-canary.0",
|
|
11
11
|
"main": "index.js",
|
|
12
12
|
"module": "index.js",
|
|
13
13
|
"publishConfig": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"@internetarchive/local-cache": "^0.2.1",
|
|
32
32
|
"@internetarchive/modal-manager": "^2.0.0",
|
|
33
33
|
"@internetarchive/shared-resize-observer": "^0.2.0",
|
|
34
|
-
"install-deps": "^1.1.0",
|
|
35
34
|
"lit": "^2.8.0 || ^3.3.2"
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
package/src/ia-book-actions.js
CHANGED
|
@@ -98,6 +98,10 @@ export default class IABookActions extends LitElement {
|
|
|
98
98
|
*/
|
|
99
99
|
this.returnNow = false;
|
|
100
100
|
|
|
101
|
+
this.loanRenewInProgress = false;
|
|
102
|
+
|
|
103
|
+
this.warningModalOpen = false;
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* contains one hour auto-loan-renew time configuration
|
|
103
107
|
* defaults to 1 hour config
|
|
@@ -141,7 +145,6 @@ export default class IABookActions extends LitElement {
|
|
|
141
145
|
* @param {string} msg
|
|
142
146
|
*/
|
|
143
147
|
sentryCaptureMsg(msg) {
|
|
144
|
-
log(window?.Sentry);
|
|
145
148
|
window?.Sentry?.captureMessage(msg);
|
|
146
149
|
}
|
|
147
150
|
|
|
@@ -233,7 +236,7 @@ export default class IABookActions extends LitElement {
|
|
|
233
236
|
'browsingExpired' in this.lendingStatus &&
|
|
234
237
|
this.lendingStatus?.browsingExpired;
|
|
235
238
|
if (hasExpired) {
|
|
236
|
-
log('
|
|
239
|
+
log('[IABookActions] browsing expired — showing expired state');
|
|
237
240
|
|
|
238
241
|
if (!this.tokenPoller) {
|
|
239
242
|
this.sentryCaptureMsg(sentryLogs.bookWasExpired);
|
|
@@ -290,7 +293,11 @@ export default class IABookActions extends LitElement {
|
|
|
290
293
|
* dispatched this event from bookreader page changed
|
|
291
294
|
*/
|
|
292
295
|
window.addEventListener('BookReader:userAction', () => {
|
|
293
|
-
|
|
296
|
+
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
297
|
+
if (this.lendingStatus.browsingExpired) {
|
|
298
|
+
this.autoRenewExpiredLoan();
|
|
299
|
+
}
|
|
300
|
+
|
|
294
301
|
if (this.borrowType === 'browsed') {
|
|
295
302
|
this.autoLoanRenewChecker(true);
|
|
296
303
|
}
|
|
@@ -303,17 +310,17 @@ export default class IABookActions extends LitElement {
|
|
|
303
310
|
*/
|
|
304
311
|
document.addEventListener('visibilitychange', async () => {
|
|
305
312
|
if (!document.hidden) {
|
|
306
|
-
log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
) {
|
|
313
|
+
log('[IABookActions] visibilitychange:', this.borrowType);
|
|
314
|
+
|
|
315
|
+
// Loan already expired while tab was hidden — try to silently renew
|
|
316
|
+
if (this.lendingStatus.browsingExpired === true) {
|
|
317
|
+
this.autoRenewExpiredLoan();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.borrowType !== 'browsed') return;
|
|
322
|
+
|
|
323
|
+
if (this.lendingStatus.browsingExpired === false) {
|
|
317
324
|
const loanTime = await this.localCache.get(
|
|
318
325
|
`${this.identifier}-loanTime`
|
|
319
326
|
);
|
|
@@ -324,7 +331,8 @@ export default class IABookActions extends LitElement {
|
|
|
324
331
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
325
332
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
326
333
|
} else {
|
|
327
|
-
|
|
334
|
+
// Loan expired while user was away — try to silently renew
|
|
335
|
+
this.autoRenewExpiredLoan();
|
|
328
336
|
this.disconnectedCallback();
|
|
329
337
|
}
|
|
330
338
|
}
|
|
@@ -350,6 +358,36 @@ export default class IABookActions extends LitElement {
|
|
|
350
358
|
this.loanRenewResult = this.loanRenewHelper.result;
|
|
351
359
|
}
|
|
352
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Attempt to automatically renew a browse loan that has expired.
|
|
363
|
+
* Called on visibilitychange (user returns to tab) or on page turn when
|
|
364
|
+
* the loan has expired but the book may still be available.
|
|
365
|
+
*
|
|
366
|
+
* Reuses the same renew_loan path as the "Keep reading" warning modal button.
|
|
367
|
+
* On success: handleLoanAutoRenewed() resets the timer seamlessly.
|
|
368
|
+
* On error: handleLendingActionError() shows showLoanUnavailableModal().
|
|
369
|
+
*/
|
|
370
|
+
autoRenewExpiredLoan() {
|
|
371
|
+
if (this.loanRenewInProgress) return;
|
|
372
|
+
this.loanRenewInProgress = true;
|
|
373
|
+
|
|
374
|
+
this.modal?.closeModal();
|
|
375
|
+
|
|
376
|
+
// Optimistically flip browsingExpired back to false so the action bar
|
|
377
|
+
// stays red (patronIsReadingAction) while the renew_loan request is in
|
|
378
|
+
// flight, instead of showing the blue "Borrow" state.
|
|
379
|
+
this.lendingStatus = {
|
|
380
|
+
...this.lendingStatus,
|
|
381
|
+
browsingExpired: false,
|
|
382
|
+
secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Defer renewNow until after lendingStatus timer setup completes.
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
this.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
388
|
+
}, 0);
|
|
389
|
+
}
|
|
390
|
+
|
|
353
391
|
/**
|
|
354
392
|
* Required as sibling on page
|
|
355
393
|
* @returns HTMLElement
|
|
@@ -365,29 +403,34 @@ export default class IABookActions extends LitElement {
|
|
|
365
403
|
* Show the waring modal to ask user if they are still reading
|
|
366
404
|
*/
|
|
367
405
|
async showWarningModal() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
if (this.warningModalOpen) return;
|
|
407
|
+
this.warningModalOpen = true;
|
|
408
|
+
|
|
409
|
+
log('[IABookActions] showWarningModal');
|
|
410
|
+
|
|
411
|
+
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
412
|
+
const {
|
|
413
|
+
texts: warningTexts,
|
|
414
|
+
secondsLeft: rawSecondsLeft,
|
|
415
|
+
} = this.loanRenewResult;
|
|
416
|
+
let secondsLeft = rawSecondsLeft;
|
|
417
|
+
if (secondsLeft === undefined || secondsLeft <= 0) {
|
|
377
418
|
secondsLeft = this.lendingStatus.secondsLeftOnLoan;
|
|
378
419
|
} else {
|
|
379
420
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
380
421
|
}
|
|
381
422
|
|
|
423
|
+
// clear modal and reset renew state
|
|
424
|
+
this.modal.customModalContent = nothing;
|
|
425
|
+
this.modal?.closeModal();
|
|
426
|
+
this.loanRenewResult = { texts: '', renewNow: false };
|
|
427
|
+
|
|
382
428
|
const config = new ModalConfig({
|
|
383
429
|
headline: 'Are you still reading?',
|
|
384
430
|
headerColor: '#194880',
|
|
385
431
|
showCloseButton: false,
|
|
386
432
|
closeOnBackdropClick: false,
|
|
387
|
-
message: this.loanRenewHelper?.getMessageTexts(
|
|
388
|
-
this.loanRenewResult.texts,
|
|
389
|
-
secondsLeft
|
|
390
|
-
),
|
|
433
|
+
message: this.loanRenewHelper?.getMessageTexts(warningTexts, secondsLeft),
|
|
391
434
|
});
|
|
392
435
|
|
|
393
436
|
const customModalContent = html`<br />
|
|
@@ -484,6 +527,7 @@ export default class IABookActions extends LitElement {
|
|
|
484
527
|
|
|
485
528
|
/**
|
|
486
529
|
* Show modal when book is auto returned
|
|
530
|
+
* @deprecated
|
|
487
531
|
*/
|
|
488
532
|
async showExpiredModal() {
|
|
489
533
|
const config = new ModalConfig({
|
|
@@ -513,7 +557,7 @@ export default class IABookActions extends LitElement {
|
|
|
513
557
|
* Execute when loan is expired
|
|
514
558
|
*/
|
|
515
559
|
async browseHasExpired() {
|
|
516
|
-
log('
|
|
560
|
+
log('[IABookActions] browseHasExpired');
|
|
517
561
|
window?.IALendingIntervals?.clearAll();
|
|
518
562
|
|
|
519
563
|
const currStatus = {
|
|
@@ -532,31 +576,52 @@ export default class IABookActions extends LitElement {
|
|
|
532
576
|
this.loanRenewResult.texts =
|
|
533
577
|
'This book has been returned due to inactivity.';
|
|
534
578
|
|
|
535
|
-
|
|
579
|
+
this.modal?.closeModal();
|
|
536
580
|
|
|
537
581
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
538
582
|
}
|
|
539
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Show modal when the book can no longer be automatically renewed
|
|
586
|
+
* because another patron has checked it out.
|
|
587
|
+
*/
|
|
588
|
+
async showLoanUnavailableModal() {
|
|
589
|
+
const config = new ModalConfig({
|
|
590
|
+
headline: '',
|
|
591
|
+
showCloseButton: false,
|
|
592
|
+
closeOnBackdropClick: false,
|
|
593
|
+
headerColor: '#194880',
|
|
594
|
+
message:
|
|
595
|
+
'Another patron is borrowing this book. Please check back later.',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const customModalContent = html`<br />
|
|
599
|
+
<div style="text-align: center">
|
|
600
|
+
<button
|
|
601
|
+
style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
|
|
602
|
+
@click=${() => URLHelper.goToUrl(this.returnUrl, true)}
|
|
603
|
+
>
|
|
604
|
+
Okay
|
|
605
|
+
</button>
|
|
606
|
+
</div>`;
|
|
607
|
+
|
|
608
|
+
await this.modal?.showModal({ config, customModalContent });
|
|
609
|
+
}
|
|
610
|
+
|
|
540
611
|
async startBrowseTimer() {
|
|
541
612
|
window?.IALendingIntervals?.clearBrowseExpireTimeout();
|
|
542
613
|
|
|
543
|
-
const {
|
|
544
|
-
|
|
614
|
+
const {
|
|
615
|
+
browsingExpired,
|
|
616
|
+
user_has_browsed,
|
|
617
|
+
secondsLeftOnLoan,
|
|
618
|
+
} = this.lendingStatus;
|
|
545
619
|
|
|
546
620
|
if (!user_has_browsed || browsingExpired) {
|
|
547
|
-
log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
|
|
548
|
-
user_has_browsed,
|
|
549
|
-
browsingExpired,
|
|
550
|
-
secondsLeftOnLoan,
|
|
551
|
-
});
|
|
552
621
|
return;
|
|
553
622
|
}
|
|
554
623
|
|
|
555
624
|
window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
|
|
556
|
-
log(
|
|
557
|
-
'startBrowseTimer > browseExpireTimeout --- will expire loan',
|
|
558
|
-
secondsLeftOnLoan
|
|
559
|
-
);
|
|
560
625
|
this.browseHasExpired();
|
|
561
626
|
}, secondsLeftOnLoan * 1000);
|
|
562
627
|
}
|
|
@@ -638,12 +703,9 @@ export default class IABookActions extends LitElement {
|
|
|
638
703
|
|
|
639
704
|
// number of seconds left in current loan
|
|
640
705
|
const secondsLeft = Math.round((loanTime - new Date()) / 1000);
|
|
641
|
-
log(
|
|
642
|
-
|
|
643
|
-
log('IABookActions: handleLoanAutoRenewed --- ', {
|
|
706
|
+
log('[IABookActions] handleLoanAutoRenewed', {
|
|
707
|
+
secondsLeft,
|
|
644
708
|
ajaxResponse: detail?.data,
|
|
645
|
-
loanRenewResult: this.loanRenewResult,
|
|
646
|
-
secondsLeftOnLoan: secondsLeft,
|
|
647
709
|
});
|
|
648
710
|
|
|
649
711
|
const currStatus = {
|
|
@@ -660,6 +722,9 @@ export default class IABookActions extends LitElement {
|
|
|
660
722
|
this.modal.customModalContent = nothing;
|
|
661
723
|
this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
|
|
662
724
|
}
|
|
725
|
+
|
|
726
|
+
this.warningModalOpen = false;
|
|
727
|
+
this.loanRenewInProgress = false;
|
|
663
728
|
}
|
|
664
729
|
|
|
665
730
|
/**
|
|
@@ -695,29 +760,19 @@ export default class IABookActions extends LitElement {
|
|
|
695
760
|
// re-sync timer if gone off because of background window
|
|
696
761
|
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
697
762
|
// if really updated, escape from here
|
|
698
|
-
const resyncd =
|
|
763
|
+
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
699
764
|
|
|
700
765
|
if (resyncd.hasSynced) {
|
|
701
766
|
secondsLeft = resyncd.whatShouldLeft;
|
|
702
|
-
log('startTimerCountdown
|
|
767
|
+
log('[IABookActions] startTimerCountdown: timer re-synced', {
|
|
703
768
|
secondsLeft,
|
|
704
769
|
});
|
|
705
770
|
}
|
|
706
771
|
|
|
707
|
-
log(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
secondsLeft,
|
|
712
|
-
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
713
|
-
},
|
|
714
|
-
'time: ',
|
|
715
|
-
new Date().getMinutes(),
|
|
716
|
-
':',
|
|
717
|
-
new Date().getSeconds(),
|
|
718
|
-
', timerDelay: ',
|
|
719
|
-
this.timerExecutionSeconds
|
|
720
|
-
);
|
|
772
|
+
log('[IABookActions] startTimerCountdown', {
|
|
773
|
+
secondsLeft,
|
|
774
|
+
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
775
|
+
});
|
|
721
776
|
|
|
722
777
|
/**
|
|
723
778
|
* execute from last 10th minutes to 0th minute
|
|
@@ -742,7 +797,7 @@ export default class IABookActions extends LitElement {
|
|
|
742
797
|
* @param {number} timerSecondsLeft - actual seconds left get from setInterval
|
|
743
798
|
* @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
|
|
744
799
|
*/
|
|
745
|
-
|
|
800
|
+
reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
746
801
|
const currentTime = new Date();
|
|
747
802
|
|
|
748
803
|
// current time - loan time
|
|
@@ -752,27 +807,11 @@ export default class IABookActions extends LitElement {
|
|
|
752
807
|
const secondsShouldLeft =
|
|
753
808
|
this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
|
|
754
809
|
|
|
755
|
-
log('currentTime: ', currentTime);
|
|
756
|
-
log('timeWhenTimerStart: ', this.timeWhenTimerStart);
|
|
757
|
-
log('diffInSeconds: ', diffInSeconds);
|
|
758
|
-
log('secondsShouldLeft: ', secondsShouldLeft);
|
|
759
|
-
log(
|
|
760
|
-
'this.lendingStatus.secondsLeftOnLoan: ',
|
|
761
|
-
this.lendingStatus.secondsLeftOnLoan
|
|
762
|
-
);
|
|
763
|
-
|
|
764
810
|
// convert in minutes
|
|
765
811
|
const whatIsleft = Math.round(timerSecondsLeft);
|
|
766
812
|
const whatShouldLeft = Math.round(secondsShouldLeft);
|
|
767
813
|
const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
|
|
768
814
|
|
|
769
|
-
log(`reSyncTimerIfGoneOff?`, {
|
|
770
|
-
whatIsleft,
|
|
771
|
-
whatShouldLeft,
|
|
772
|
-
timerElSeconds,
|
|
773
|
-
timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
|
|
774
|
-
});
|
|
775
|
-
|
|
776
815
|
if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
|
|
777
816
|
// set lending status with new time to update decrementor
|
|
778
817
|
const currStatus = {
|
|
@@ -783,7 +822,9 @@ export default class IABookActions extends LitElement {
|
|
|
783
822
|
}
|
|
784
823
|
|
|
785
824
|
if (whatIsleft !== whatShouldLeft) {
|
|
786
|
-
log(
|
|
825
|
+
log(
|
|
826
|
+
`[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
|
|
827
|
+
);
|
|
787
828
|
return { hasSynced: true, whatShouldLeft };
|
|
788
829
|
}
|
|
789
830
|
|
|
@@ -799,10 +840,6 @@ export default class IABookActions extends LitElement {
|
|
|
799
840
|
* @memberof IABookActions
|
|
800
841
|
*/
|
|
801
842
|
async loanRenewAttempt(secondsLeft) {
|
|
802
|
-
log('loanRenewAttempt ---', {
|
|
803
|
-
secondsLeft,
|
|
804
|
-
loanRenewResult: this.loanRenewResult,
|
|
805
|
-
});
|
|
806
843
|
let loanSecondsLeft = secondsLeft;
|
|
807
844
|
/**
|
|
808
845
|
* auto-renew is not possible in last seconds (let say 50 second) because,
|
|
@@ -812,7 +849,7 @@ export default class IABookActions extends LitElement {
|
|
|
812
849
|
* so if seconds left is < 50, just expire the loan
|
|
813
850
|
*/
|
|
814
851
|
if (loanSecondsLeft < 50) {
|
|
815
|
-
log('loanRenewAttempt
|
|
852
|
+
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
816
853
|
await this.browseHasExpired();
|
|
817
854
|
return;
|
|
818
855
|
}
|
|
@@ -894,9 +931,17 @@ export default class IABookActions extends LitElement {
|
|
|
894
931
|
const action = event?.detail?.action;
|
|
895
932
|
const errorMsg = event?.detail?.data?.error;
|
|
896
933
|
|
|
897
|
-
//
|
|
898
|
-
if (
|
|
934
|
+
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
935
|
+
if (
|
|
936
|
+
errorMsg &&
|
|
937
|
+
action === 'renew_loan' &&
|
|
938
|
+
errorMsg.match(/not available/)
|
|
939
|
+
) {
|
|
940
|
+
this.loanRenewInProgress = false;
|
|
941
|
+
this.showLoanUnavailableModal();
|
|
942
|
+
} else if (errorMsg && action !== 'create_token') {
|
|
899
943
|
this.showErrorModal(errorMsg, action);
|
|
944
|
+
}
|
|
900
945
|
|
|
901
946
|
// if error related to loan token
|
|
902
947
|
// - set user_has_browsed to `false`
|
|
@@ -82,7 +82,7 @@ describe('<ia-book-actions>', () => {
|
|
|
82
82
|
expect(el.disableActionGroup).to.be.true;
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it('handles `BookReader:userAction` event', async () => {
|
|
85
|
+
it('handles `BookReader:userAction` event when loan is active', async () => {
|
|
86
86
|
const el = await fixture(
|
|
87
87
|
container({
|
|
88
88
|
userid: '@user1',
|
|
@@ -91,8 +91,10 @@ describe('<ia-book-actions>', () => {
|
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
const spy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
94
|
-
el.
|
|
94
|
+
el.lendingStatus = { ...el.lendingStatus, browsingExpired: false };
|
|
95
95
|
await el.updateComplete;
|
|
96
|
+
// Set borrowType after the update so setupLendingToolbarActions() doesn't overwrite it
|
|
97
|
+
el.borrowType = 'browsed';
|
|
96
98
|
|
|
97
99
|
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
98
100
|
await el.updateComplete;
|
|
@@ -100,6 +102,25 @@ describe('<ia-book-actions>', () => {
|
|
|
100
102
|
expect(spy.calledOnce).to.be.true;
|
|
101
103
|
expect(spy.calledWith(true)).to.be.true;
|
|
102
104
|
});
|
|
105
|
+
|
|
106
|
+
it('handles `BookReader:userAction` event when loan has expired', async () => {
|
|
107
|
+
const el = await fixture(
|
|
108
|
+
container({
|
|
109
|
+
userid: '@user1',
|
|
110
|
+
identifier: 'foobar',
|
|
111
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
116
|
+
el.borrowType = 'browsed';
|
|
117
|
+
await el.updateComplete;
|
|
118
|
+
|
|
119
|
+
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
120
|
+
await el.updateComplete;
|
|
121
|
+
|
|
122
|
+
expect(spy.calledOnce).to.be.true;
|
|
123
|
+
});
|
|
103
124
|
});
|
|
104
125
|
|
|
105
126
|
describe('Primary Actions data', () => {
|
|
@@ -467,7 +488,7 @@ describe('Visibility change API for document', async () => {
|
|
|
467
488
|
});
|
|
468
489
|
});
|
|
469
490
|
|
|
470
|
-
it('when book is not expired', async () => {
|
|
491
|
+
it('when book is not expired and loan time is valid', async () => {
|
|
471
492
|
const el = await fixture(
|
|
472
493
|
container({
|
|
473
494
|
userid: '@user1',
|
|
@@ -480,31 +501,220 @@ describe('Visibility change API for document', async () => {
|
|
|
480
501
|
);
|
|
481
502
|
await el.updateComplete;
|
|
482
503
|
|
|
504
|
+
// Set a valid loan time well into the future
|
|
505
|
+
await el.localCache.set({
|
|
506
|
+
key: 'foobar-loanTime',
|
|
507
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
508
|
+
ttl: 3600,
|
|
509
|
+
});
|
|
510
|
+
|
|
483
511
|
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
484
512
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
513
|
+
await aTimeout(100);
|
|
514
|
+
|
|
515
|
+
expect(spy.calledOnce).to.be.true;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('when loan expired while tab was hidden (browsingExpired false, loanTime past)', async () => {
|
|
519
|
+
const el = await fixture(
|
|
520
|
+
container({
|
|
521
|
+
userid: '@user1',
|
|
522
|
+
identifier: 'foobar',
|
|
523
|
+
lendingStatus: {
|
|
524
|
+
user_has_browsed: true,
|
|
525
|
+
browsingExpired: false,
|
|
526
|
+
},
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
await el.updateComplete;
|
|
530
|
+
|
|
531
|
+
// Set a loan time already in the past
|
|
532
|
+
await el.localCache.set({
|
|
533
|
+
key: 'foobar-loanTime',
|
|
534
|
+
value: new Date(new Date().getTime() - 10 * 1000),
|
|
535
|
+
ttl: 3600,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
539
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
540
|
+
await aTimeout(100);
|
|
485
541
|
|
|
486
542
|
expect(spy.calledOnce).to.be.true;
|
|
487
543
|
});
|
|
488
544
|
|
|
489
|
-
it('when
|
|
545
|
+
it('when loan already marked expired (browsingExpired true)', async () => {
|
|
490
546
|
const el = await fixture(
|
|
491
547
|
container({
|
|
492
548
|
userid: '@user1',
|
|
493
549
|
identifier: 'foobar',
|
|
494
550
|
lendingStatus: {
|
|
495
551
|
user_has_browsed: true,
|
|
552
|
+
browsingExpired: true,
|
|
496
553
|
},
|
|
497
554
|
})
|
|
498
555
|
);
|
|
499
556
|
await el.updateComplete;
|
|
500
557
|
|
|
501
|
-
|
|
558
|
+
el.borrowType = 'browsed';
|
|
559
|
+
|
|
560
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
502
561
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
562
|
+
await aTimeout(100);
|
|
503
563
|
|
|
504
564
|
expect(spy.calledOnce).to.be.true;
|
|
505
565
|
});
|
|
506
566
|
});
|
|
507
567
|
|
|
568
|
+
describe('autoRenewExpiredLoan', () => {
|
|
569
|
+
it('optimistically resets browsingExpired so the button stays red', async () => {
|
|
570
|
+
const el = await fixture(
|
|
571
|
+
container({
|
|
572
|
+
userid: '@user1',
|
|
573
|
+
identifier: 'foobar',
|
|
574
|
+
lendingStatus: {
|
|
575
|
+
user_has_browsed: true,
|
|
576
|
+
available_to_browse: true,
|
|
577
|
+
browsingExpired: true,
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
await el.updateComplete;
|
|
582
|
+
|
|
583
|
+
// Before: browsingExpired true → borrow1HrAction → primary color is 'primary' (blue)
|
|
584
|
+
expect(el.primaryColor).to.equal('primary');
|
|
585
|
+
|
|
586
|
+
el.autoRenewExpiredLoan();
|
|
587
|
+
await el.updateComplete;
|
|
588
|
+
|
|
589
|
+
// After: browsingExpired flipped to false → patronIsReadingAction → 'danger' (red)
|
|
590
|
+
expect(el.lendingStatus.browsingExpired).to.be.false;
|
|
591
|
+
expect(el.primaryColor).to.equal('danger');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
|
|
595
|
+
const el = await fixture(
|
|
596
|
+
container({
|
|
597
|
+
userid: '@user1',
|
|
598
|
+
identifier: 'foobar',
|
|
599
|
+
lendingStatus: {
|
|
600
|
+
user_has_browsed: true,
|
|
601
|
+
browsingExpired: true,
|
|
602
|
+
},
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
await el.updateComplete;
|
|
606
|
+
|
|
607
|
+
el.autoRenewExpiredLoan();
|
|
608
|
+
|
|
609
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
610
|
+
|
|
611
|
+
// renewNow is set in a setTimeout — flush the macrotask queue
|
|
612
|
+
await aTimeout(50);
|
|
613
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
614
|
+
expect(el.loanRenewResult.renewType).to.equal('auto');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('ignores subsequent calls while a renewal is already in progress', async () => {
|
|
618
|
+
const el = await fixture(
|
|
619
|
+
container({
|
|
620
|
+
userid: '@user1',
|
|
621
|
+
identifier: 'foobar',
|
|
622
|
+
lendingStatus: {
|
|
623
|
+
user_has_browsed: true,
|
|
624
|
+
browsingExpired: true,
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
await el.updateComplete;
|
|
629
|
+
|
|
630
|
+
// Spy *after* fixture so the handler is the same instance
|
|
631
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
632
|
+
|
|
633
|
+
el.autoRenewExpiredLoan(); // first call — proceeds
|
|
634
|
+
el.autoRenewExpiredLoan(); // second call — should be a no-op
|
|
635
|
+
|
|
636
|
+
// The spy counts both invocations but the internal guard stops the second
|
|
637
|
+
expect(spy.callCount).to.equal(2);
|
|
638
|
+
// loanRenewInProgress is still true (not double-set or cleared)
|
|
639
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
640
|
+
|
|
641
|
+
// Only one setTimeout fires (the second call returned early)
|
|
642
|
+
await aTimeout(50);
|
|
643
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
|
|
647
|
+
const el = await fixture(
|
|
648
|
+
container({
|
|
649
|
+
userid: '@user1',
|
|
650
|
+
identifier: 'foobar',
|
|
651
|
+
lendingStatus: {
|
|
652
|
+
user_has_browsed: true,
|
|
653
|
+
browsingExpired: false,
|
|
654
|
+
secondsLeftOnLoan: 100,
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
await el.updateComplete;
|
|
659
|
+
|
|
660
|
+
await el.localCache.set({
|
|
661
|
+
key: 'foobar-loanTime',
|
|
662
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
663
|
+
ttl: 3600,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
el.autoRenewExpiredLoan();
|
|
667
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
668
|
+
|
|
669
|
+
// Wait for the deferred renewNow to be set
|
|
670
|
+
await aTimeout(50);
|
|
671
|
+
|
|
672
|
+
// Simulate the renew_loan success response
|
|
673
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
674
|
+
'collapsible-action-group'
|
|
675
|
+
);
|
|
676
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
677
|
+
new CustomEvent('loanAutoRenewed', {
|
|
678
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
await aTimeout(100);
|
|
683
|
+
await el.updateComplete;
|
|
684
|
+
|
|
685
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
689
|
+
const el = await fixture(
|
|
690
|
+
container({
|
|
691
|
+
userid: '@user1',
|
|
692
|
+
identifier: 'foobar',
|
|
693
|
+
lendingStatus: {
|
|
694
|
+
user_has_browsed: true,
|
|
695
|
+
browsingExpired: true,
|
|
696
|
+
},
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
await el.updateComplete;
|
|
700
|
+
|
|
701
|
+
const showModalSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
702
|
+
|
|
703
|
+
el.autoRenewExpiredLoan();
|
|
704
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
705
|
+
|
|
706
|
+
el.handleLendingActionError({
|
|
707
|
+
detail: {
|
|
708
|
+
action: 'renew_loan',
|
|
709
|
+
data: { error: 'book is not available' },
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
714
|
+
expect(showModalSpy.calledOnce).to.be.true;
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
508
718
|
describe('Shared Resize Observer', () => {
|
|
509
719
|
it('can receive a Shared Resize Observer', async () => {
|
|
510
720
|
const sharedObserverStub = new SharedResizeObserver();
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(yarn outdated *)",
|
|
5
|
-
"Bash(gh pr *)",
|
|
6
|
-
"Bash(git log *)",
|
|
7
|
-
"mcp__plugin_atlassian_atlassian__getAccessibleAtlassianResources",
|
|
8
|
-
"Bash(npm view *)",
|
|
9
|
-
"mcp__plugin_atlassian_atlassian__searchJiraIssuesUsingJql",
|
|
10
|
-
"Bash(xargs -I {} basename {})",
|
|
11
|
-
"Bash(npm info *)",
|
|
12
|
-
"mcp__plugin_atlassian_atlassian__getJiraIssue",
|
|
13
|
-
"mcp__plugin_atlassian_atlassian__getJiraProjectIssueTypesMetadata"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
}
|