@internetarchive/ia-book-actions 1.4.3-canary.1 → 1.4.3
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/.github/workflows/pr-preview.yml +4 -0
- package/package.json +2 -2
- package/src/components/timer-countdown.js +3 -13
- package/src/core/services/actions-handler/actions-handler.js +2 -1
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +1 -1
- package/src/ia-book-actions.js +87 -132
- package/test/core/services/actions-handler.test.js +91 -0
- package/test/ia-book-actions.test.js +5 -215
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-account-settings.git"
|
|
7
7
|
},
|
|
8
8
|
"author": "Internet Archive",
|
|
9
9
|
"license": "AGPL-3.0-only",
|
|
10
|
-
"version": "1.4.3
|
|
10
|
+
"version": "1.4.3",
|
|
11
11
|
"main": "index.js",
|
|
12
12
|
"module": "index.js",
|
|
13
13
|
"publishConfig": {
|
|
@@ -46,16 +46,6 @@ export default class TimerCountdown extends LitElement {
|
|
|
46
46
|
: `${timeLeft} ${unitOfTime}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
updated(changedProperties) {
|
|
50
|
-
if (changedProperties.has('secondsLeftOnLoan')) {
|
|
51
|
-
const prev = changedProperties.get('secondsLeftOnLoan');
|
|
52
|
-
// Loan was renewed (timer reset from expired/zero back to active)
|
|
53
|
-
if (prev <= 0 && this.secondsLeftOnLoan > 0) {
|
|
54
|
-
this.displayTime = false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
49
|
render() {
|
|
60
50
|
const viewClass = this.displayTime ? 'view' : 'hide';
|
|
61
51
|
return html`
|
|
@@ -63,8 +53,8 @@ export default class TimerCountdown extends LitElement {
|
|
|
63
53
|
id="timer-counter"
|
|
64
54
|
class=${viewClass}
|
|
65
55
|
@click=${() => {
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
this.displayTime = !this.displayTime;
|
|
57
|
+
}}
|
|
68
58
|
role="timer"
|
|
69
59
|
>
|
|
70
60
|
<span>${this.minutesLeftOnLoan} - </span>
|
|
@@ -98,7 +88,7 @@ export default class TimerCountdown extends LitElement {
|
|
|
98
88
|
}
|
|
99
89
|
|
|
100
90
|
.hide {
|
|
101
|
-
opacity:
|
|
91
|
+
opacity: 0;
|
|
102
92
|
}
|
|
103
93
|
|
|
104
94
|
.show {
|
|
@@ -409,6 +409,7 @@ export default class ActionsHandler extends LitElement {
|
|
|
409
409
|
*/
|
|
410
410
|
setStickyAdminAccess(value) {
|
|
411
411
|
const domain = window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
|
|
412
|
-
|
|
412
|
+
const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
|
|
413
|
+
Cookies.setItem('sticky-admin-access', value, expires, '/', domain);
|
|
413
414
|
}
|
|
414
415
|
}
|
package/src/ia-book-actions.js
CHANGED
|
@@ -98,10 +98,6 @@ 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
|
-
|
|
105
101
|
/**
|
|
106
102
|
* contains one hour auto-loan-renew time configuration
|
|
107
103
|
* defaults to 1 hour config
|
|
@@ -145,6 +141,7 @@ export default class IABookActions extends LitElement {
|
|
|
145
141
|
* @param {string} msg
|
|
146
142
|
*/
|
|
147
143
|
sentryCaptureMsg(msg) {
|
|
144
|
+
log(window?.Sentry);
|
|
148
145
|
window?.Sentry?.captureMessage(msg);
|
|
149
146
|
}
|
|
150
147
|
|
|
@@ -236,7 +233,7 @@ export default class IABookActions extends LitElement {
|
|
|
236
233
|
'browsingExpired' in this.lendingStatus &&
|
|
237
234
|
this.lendingStatus?.browsingExpired;
|
|
238
235
|
if (hasExpired) {
|
|
239
|
-
log('
|
|
236
|
+
log('setupLendingToolbarActions > hasExpired --- ');
|
|
240
237
|
|
|
241
238
|
if (!this.tokenPoller) {
|
|
242
239
|
this.sentryCaptureMsg(sentryLogs.bookWasExpired);
|
|
@@ -293,11 +290,7 @@ export default class IABookActions extends LitElement {
|
|
|
293
290
|
* dispatched this event from bookreader page changed
|
|
294
291
|
*/
|
|
295
292
|
window.addEventListener('BookReader:userAction', () => {
|
|
296
|
-
|
|
297
|
-
if (this.lendingStatus.browsingExpired) {
|
|
298
|
-
this.autoRenewExpiredLoan();
|
|
299
|
-
}
|
|
300
|
-
|
|
293
|
+
log('IABookActions:BookReader:userAction');
|
|
301
294
|
if (this.borrowType === 'browsed') {
|
|
302
295
|
this.autoLoanRenewChecker(true);
|
|
303
296
|
}
|
|
@@ -310,17 +303,17 @@ export default class IABookActions extends LitElement {
|
|
|
310
303
|
*/
|
|
311
304
|
document.addEventListener('visibilitychange', async () => {
|
|
312
305
|
if (!document.hidden) {
|
|
313
|
-
log(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
306
|
+
log(
|
|
307
|
+
'visibilitychange event execute:------------------ ',
|
|
308
|
+
new Date().getMinutes(),
|
|
309
|
+
new Date().getSeconds(),
|
|
310
|
+
this.borrowType
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
this.borrowType === 'browsed' &&
|
|
315
|
+
this.lendingStatus.browsingExpired === false
|
|
316
|
+
) {
|
|
324
317
|
const loanTime = await this.localCache.get(
|
|
325
318
|
`${this.identifier}-loanTime`
|
|
326
319
|
);
|
|
@@ -331,8 +324,7 @@ export default class IABookActions extends LitElement {
|
|
|
331
324
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
332
325
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
333
326
|
} else {
|
|
334
|
-
|
|
335
|
-
this.autoRenewExpiredLoan();
|
|
327
|
+
this.browseHasExpired();
|
|
336
328
|
this.disconnectedCallback();
|
|
337
329
|
}
|
|
338
330
|
}
|
|
@@ -358,36 +350,6 @@ export default class IABookActions extends LitElement {
|
|
|
358
350
|
this.loanRenewResult = this.loanRenewHelper.result;
|
|
359
351
|
}
|
|
360
352
|
|
|
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
|
-
|
|
391
353
|
/**
|
|
392
354
|
* Required as sibling on page
|
|
393
355
|
* @returns HTMLElement
|
|
@@ -403,34 +365,29 @@ export default class IABookActions extends LitElement {
|
|
|
403
365
|
* Show the waring modal to ask user if they are still reading
|
|
404
366
|
*/
|
|
405
367
|
async showWarningModal() {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
} = this.loanRenewResult;
|
|
416
|
-
let secondsLeft = rawSecondsLeft;
|
|
417
|
-
if (secondsLeft === undefined || secondsLeft <= 0) {
|
|
368
|
+
log('****** showWarningModal ******');
|
|
369
|
+
// clear modal
|
|
370
|
+
this.modal.customModalContent = nothing;
|
|
371
|
+
this.modal?.closeModal();
|
|
372
|
+
this.loanRenewResult = { texts: '', renewNow: false };
|
|
373
|
+
|
|
374
|
+
// if secondsLeft < 60, consider it 1 minute
|
|
375
|
+
let { secondsLeft } = this.loanRenewResult;
|
|
376
|
+
if (secondsLeft === undefined) {
|
|
418
377
|
secondsLeft = this.lendingStatus.secondsLeftOnLoan;
|
|
419
378
|
} else {
|
|
420
379
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
421
380
|
}
|
|
422
381
|
|
|
423
|
-
// clear modal and reset renew state
|
|
424
|
-
this.modal.customModalContent = nothing;
|
|
425
|
-
this.modal?.closeModal();
|
|
426
|
-
this.loanRenewResult = { texts: '', renewNow: false };
|
|
427
|
-
|
|
428
382
|
const config = new ModalConfig({
|
|
429
383
|
headline: 'Are you still reading?',
|
|
430
384
|
headerColor: '#194880',
|
|
431
385
|
showCloseButton: false,
|
|
432
386
|
closeOnBackdropClick: false,
|
|
433
|
-
message: this.loanRenewHelper?.getMessageTexts(
|
|
387
|
+
message: this.loanRenewHelper?.getMessageTexts(
|
|
388
|
+
this.loanRenewResult.texts,
|
|
389
|
+
secondsLeft
|
|
390
|
+
),
|
|
434
391
|
});
|
|
435
392
|
|
|
436
393
|
const customModalContent = html`<br />
|
|
@@ -527,7 +484,6 @@ export default class IABookActions extends LitElement {
|
|
|
527
484
|
|
|
528
485
|
/**
|
|
529
486
|
* Show modal when book is auto returned
|
|
530
|
-
* @deprecated
|
|
531
487
|
*/
|
|
532
488
|
async showExpiredModal() {
|
|
533
489
|
const config = new ModalConfig({
|
|
@@ -557,7 +513,7 @@ export default class IABookActions extends LitElement {
|
|
|
557
513
|
* Execute when loan is expired
|
|
558
514
|
*/
|
|
559
515
|
async browseHasExpired() {
|
|
560
|
-
log('
|
|
516
|
+
log('BrowseHasExpired ---');
|
|
561
517
|
window?.IALendingIntervals?.clearAll();
|
|
562
518
|
|
|
563
519
|
const currStatus = {
|
|
@@ -576,52 +532,31 @@ export default class IABookActions extends LitElement {
|
|
|
576
532
|
this.loanRenewResult.texts =
|
|
577
533
|
'This book has been returned due to inactivity.';
|
|
578
534
|
|
|
579
|
-
this.
|
|
535
|
+
await this.showExpiredModal();
|
|
580
536
|
|
|
581
537
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
582
538
|
}
|
|
583
539
|
|
|
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
|
-
|
|
611
540
|
async startBrowseTimer() {
|
|
612
541
|
window?.IALendingIntervals?.clearBrowseExpireTimeout();
|
|
613
542
|
|
|
614
|
-
const {
|
|
615
|
-
|
|
616
|
-
user_has_browsed,
|
|
617
|
-
secondsLeftOnLoan,
|
|
618
|
-
} = this.lendingStatus;
|
|
543
|
+
const { browsingExpired, user_has_browsed, secondsLeftOnLoan } =
|
|
544
|
+
this.lendingStatus;
|
|
619
545
|
|
|
620
546
|
if (!user_has_browsed || browsingExpired) {
|
|
547
|
+
log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
|
|
548
|
+
user_has_browsed,
|
|
549
|
+
browsingExpired,
|
|
550
|
+
secondsLeftOnLoan,
|
|
551
|
+
});
|
|
621
552
|
return;
|
|
622
553
|
}
|
|
623
554
|
|
|
624
555
|
window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
|
|
556
|
+
log(
|
|
557
|
+
'startBrowseTimer > browseExpireTimeout --- will expire loan',
|
|
558
|
+
secondsLeftOnLoan
|
|
559
|
+
);
|
|
625
560
|
this.browseHasExpired();
|
|
626
561
|
}, secondsLeftOnLoan * 1000);
|
|
627
562
|
}
|
|
@@ -703,9 +638,12 @@ export default class IABookActions extends LitElement {
|
|
|
703
638
|
|
|
704
639
|
// number of seconds left in current loan
|
|
705
640
|
const secondsLeft = Math.round((loanTime - new Date()) / 1000);
|
|
706
|
-
log(
|
|
707
|
-
|
|
641
|
+
log(loanTime, secondsLeft);
|
|
642
|
+
|
|
643
|
+
log('IABookActions: handleLoanAutoRenewed --- ', {
|
|
708
644
|
ajaxResponse: detail?.data,
|
|
645
|
+
loanRenewResult: this.loanRenewResult,
|
|
646
|
+
secondsLeftOnLoan: secondsLeft,
|
|
709
647
|
});
|
|
710
648
|
|
|
711
649
|
const currStatus = {
|
|
@@ -722,9 +660,6 @@ export default class IABookActions extends LitElement {
|
|
|
722
660
|
this.modal.customModalContent = nothing;
|
|
723
661
|
this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
|
|
724
662
|
}
|
|
725
|
-
|
|
726
|
-
this.warningModalOpen = false;
|
|
727
|
-
this.loanRenewInProgress = false;
|
|
728
663
|
}
|
|
729
664
|
|
|
730
665
|
/**
|
|
@@ -760,19 +695,29 @@ export default class IABookActions extends LitElement {
|
|
|
760
695
|
// re-sync timer if gone off because of background window
|
|
761
696
|
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
762
697
|
// if really updated, escape from here
|
|
763
|
-
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
698
|
+
const resyncd = await this.reSyncTimerIfGoneOff(secondsLeft);
|
|
764
699
|
|
|
765
700
|
if (resyncd.hasSynced) {
|
|
766
701
|
secondsLeft = resyncd.whatShouldLeft;
|
|
767
|
-
log('
|
|
702
|
+
log('startTimerCountdown --- stale, timer has resyncd', {
|
|
768
703
|
secondsLeft,
|
|
769
704
|
});
|
|
770
705
|
}
|
|
771
706
|
|
|
772
|
-
log(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
707
|
+
log(
|
|
708
|
+
'startTimerCountdown --- countdown still valid. continue...',
|
|
709
|
+
{
|
|
710
|
+
resyncd,
|
|
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
|
+
);
|
|
776
721
|
|
|
777
722
|
/**
|
|
778
723
|
* execute from last 10th minutes to 0th minute
|
|
@@ -797,7 +742,7 @@ export default class IABookActions extends LitElement {
|
|
|
797
742
|
* @param {number} timerSecondsLeft - actual seconds left get from setInterval
|
|
798
743
|
* @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
|
|
799
744
|
*/
|
|
800
|
-
reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
745
|
+
async reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
801
746
|
const currentTime = new Date();
|
|
802
747
|
|
|
803
748
|
// current time - loan time
|
|
@@ -807,11 +752,27 @@ export default class IABookActions extends LitElement {
|
|
|
807
752
|
const secondsShouldLeft =
|
|
808
753
|
this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
|
|
809
754
|
|
|
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
|
+
|
|
810
764
|
// convert in minutes
|
|
811
765
|
const whatIsleft = Math.round(timerSecondsLeft);
|
|
812
766
|
const whatShouldLeft = Math.round(secondsShouldLeft);
|
|
813
767
|
const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
|
|
814
768
|
|
|
769
|
+
log(`reSyncTimerIfGoneOff?`, {
|
|
770
|
+
whatIsleft,
|
|
771
|
+
whatShouldLeft,
|
|
772
|
+
timerElSeconds,
|
|
773
|
+
timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
|
|
774
|
+
});
|
|
775
|
+
|
|
815
776
|
if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
|
|
816
777
|
// set lending status with new time to update decrementor
|
|
817
778
|
const currStatus = {
|
|
@@ -822,9 +783,7 @@ export default class IABookActions extends LitElement {
|
|
|
822
783
|
}
|
|
823
784
|
|
|
824
785
|
if (whatIsleft !== whatShouldLeft) {
|
|
825
|
-
log(
|
|
826
|
-
`[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
|
|
827
|
-
);
|
|
786
|
+
log(`reSyncTimerIfGoneOff --- let's re-sync.`);
|
|
828
787
|
return { hasSynced: true, whatShouldLeft };
|
|
829
788
|
}
|
|
830
789
|
|
|
@@ -840,6 +799,10 @@ export default class IABookActions extends LitElement {
|
|
|
840
799
|
* @memberof IABookActions
|
|
841
800
|
*/
|
|
842
801
|
async loanRenewAttempt(secondsLeft) {
|
|
802
|
+
log('loanRenewAttempt ---', {
|
|
803
|
+
secondsLeft,
|
|
804
|
+
loanRenewResult: this.loanRenewResult,
|
|
805
|
+
});
|
|
843
806
|
let loanSecondsLeft = secondsLeft;
|
|
844
807
|
/**
|
|
845
808
|
* auto-renew is not possible in last seconds (let say 50 second) because,
|
|
@@ -849,7 +812,7 @@ export default class IABookActions extends LitElement {
|
|
|
849
812
|
* so if seconds left is < 50, just expire the loan
|
|
850
813
|
*/
|
|
851
814
|
if (loanSecondsLeft < 50) {
|
|
852
|
-
log('
|
|
815
|
+
log('loanRenewAttempt --- loanSecondsLeft < 50, will expire');
|
|
853
816
|
await this.browseHasExpired();
|
|
854
817
|
return;
|
|
855
818
|
}
|
|
@@ -931,17 +894,9 @@ export default class IABookActions extends LitElement {
|
|
|
931
894
|
const action = event?.detail?.action;
|
|
932
895
|
const errorMsg = event?.detail?.data?.error;
|
|
933
896
|
|
|
934
|
-
//
|
|
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') {
|
|
897
|
+
// template not show create_token errors
|
|
898
|
+
if (errorMsg && action !== 'create_token')
|
|
943
899
|
this.showErrorModal(errorMsg, action);
|
|
944
|
-
}
|
|
945
900
|
|
|
946
901
|
// if error related to loan token
|
|
947
902
|
// - set user_has_browsed to `false`
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { expect, fixture } from '@open-wc/testing';
|
|
2
|
+
import sinon from 'sinon';
|
|
3
|
+
|
|
4
|
+
import ActionsHandler from '../../../src/core/services/actions-handler/actions-handler.js';
|
|
5
|
+
|
|
6
|
+
// Define a temporary tag for this element for testing
|
|
7
|
+
const TEST_TAG = 'ia-actions-handler-test';
|
|
8
|
+
if (!customElements.get(TEST_TAG)) {
|
|
9
|
+
customElements.define(TEST_TAG, ActionsHandler);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('ActionsHandler#setStickyAdminAccess', () => {
|
|
13
|
+
let clock;
|
|
14
|
+
let lastSetCookie;
|
|
15
|
+
let cookieOverridden = false;
|
|
16
|
+
let actionsHandlerFixture;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
// Intercept document.cookie writes to capture the full cookie string
|
|
20
|
+
Object.defineProperty(document, 'cookie', {
|
|
21
|
+
configurable: true,
|
|
22
|
+
get() {
|
|
23
|
+
return '';
|
|
24
|
+
},
|
|
25
|
+
set(value) {
|
|
26
|
+
lastSetCookie = value;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
cookieOverridden = true;
|
|
30
|
+
|
|
31
|
+
// Create the element via fixture (portable across browsers)
|
|
32
|
+
actionsHandlerFixture = await fixture(`<${TEST_TAG}></${TEST_TAG}>`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
sinon.restore();
|
|
37
|
+
if (clock) {
|
|
38
|
+
clock.restore();
|
|
39
|
+
clock = undefined;
|
|
40
|
+
}
|
|
41
|
+
// restore document.cookie
|
|
42
|
+
if (cookieOverridden) {
|
|
43
|
+
try {
|
|
44
|
+
// remove our instance-level override
|
|
45
|
+
// eslint-disable-next-line no-param-reassign
|
|
46
|
+
delete document.cookie;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
cookieOverridden = false;
|
|
51
|
+
}
|
|
52
|
+
lastSetCookie = undefined;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('sets the sticky-admin-access cookie with correct domain, path, and 30-day expiration', async () => {
|
|
56
|
+
// Arrange: fix time and stub cookie writer
|
|
57
|
+
const now = new Date('2025-01-01T00:00:00.000Z');
|
|
58
|
+
clock = sinon.useFakeTimers({ now: now.getTime() });
|
|
59
|
+
|
|
60
|
+
// Calculate expected values based on current environment
|
|
61
|
+
const expectedDomain = window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
|
|
62
|
+
const expectedExpires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
63
|
+
|
|
64
|
+
// Act: call the method on the element created in beforeEach
|
|
65
|
+
actionsHandlerFixture.setStickyAdminAccess(true);
|
|
66
|
+
|
|
67
|
+
// Assert: verify the composed cookie string
|
|
68
|
+
expect(lastSetCookie).to.be.a('string');
|
|
69
|
+
// Name and value
|
|
70
|
+
expect(lastSetCookie).to.contain(`${encodeURIComponent('sticky-admin-access')}=${encodeURIComponent('true')}`);
|
|
71
|
+
// Domain
|
|
72
|
+
expect(lastSetCookie).to.contain(`domain=${expectedDomain}`);
|
|
73
|
+
// Path
|
|
74
|
+
expect(lastSetCookie).to.contain('path=/');
|
|
75
|
+
// Expiration close to 30 days from now
|
|
76
|
+
const match = /expires=([^;]+)/.exec(lastSetCookie);
|
|
77
|
+
expect(match).to.not.equal(null);
|
|
78
|
+
const expiresStr = match && match[1];
|
|
79
|
+
const parsed = expiresStr ? new Date(expiresStr) : null;
|
|
80
|
+
expect(parsed).to.be.instanceOf(Date);
|
|
81
|
+
expect(parsed && parsed.getTime()).to.equal(expectedExpires.getTime());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('sets the cookie value to false when disabling', async () => {
|
|
85
|
+
// Act
|
|
86
|
+
actionsHandlerFixture.setStickyAdminAccess(false);
|
|
87
|
+
|
|
88
|
+
expect(lastSetCookie).to.be.a('string');
|
|
89
|
+
expect(lastSetCookie).to.contain(`${encodeURIComponent('sticky-admin-access')}=${encodeURIComponent('false')}`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -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
|
|
85
|
+
it('handles `BookReader:userAction` event', async () => {
|
|
86
86
|
const el = await fixture(
|
|
87
87
|
container({
|
|
88
88
|
userid: '@user1',
|
|
@@ -91,28 +91,6 @@ describe('<ia-book-actions>', () => {
|
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
const spy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
94
|
-
el.lendingStatus = { ...el.lendingStatus, browsingExpired: false };
|
|
95
|
-
await el.updateComplete;
|
|
96
|
-
// Set borrowType after the update so setupLendingToolbarActions() doesn't overwrite it
|
|
97
|
-
el.borrowType = 'browsed';
|
|
98
|
-
|
|
99
|
-
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
100
|
-
await el.updateComplete;
|
|
101
|
-
|
|
102
|
-
expect(spy.calledOnce).to.be.true;
|
|
103
|
-
expect(spy.calledWith(true)).to.be.true;
|
|
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
94
|
el.borrowType = 'browsed';
|
|
117
95
|
await el.updateComplete;
|
|
118
96
|
|
|
@@ -120,6 +98,7 @@ describe('<ia-book-actions>', () => {
|
|
|
120
98
|
await el.updateComplete;
|
|
121
99
|
|
|
122
100
|
expect(spy.calledOnce).to.be.true;
|
|
101
|
+
expect(spy.calledWith(true)).to.be.true;
|
|
123
102
|
});
|
|
124
103
|
});
|
|
125
104
|
|
|
@@ -488,7 +467,7 @@ describe('Visibility change API for document', async () => {
|
|
|
488
467
|
});
|
|
489
468
|
});
|
|
490
469
|
|
|
491
|
-
it('when book is not expired
|
|
470
|
+
it('when book is not expired', async () => {
|
|
492
471
|
const el = await fixture(
|
|
493
472
|
container({
|
|
494
473
|
userid: '@user1',
|
|
@@ -501,220 +480,31 @@ describe('Visibility change API for document', async () => {
|
|
|
501
480
|
);
|
|
502
481
|
await el.updateComplete;
|
|
503
482
|
|
|
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
|
-
|
|
511
483
|
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
512
484
|
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);
|
|
541
485
|
|
|
542
486
|
expect(spy.calledOnce).to.be.true;
|
|
543
487
|
});
|
|
544
488
|
|
|
545
|
-
it('when
|
|
489
|
+
it('when book is already expired', async () => {
|
|
546
490
|
const el = await fixture(
|
|
547
491
|
container({
|
|
548
492
|
userid: '@user1',
|
|
549
493
|
identifier: 'foobar',
|
|
550
494
|
lendingStatus: {
|
|
551
495
|
user_has_browsed: true,
|
|
552
|
-
browsingExpired: true,
|
|
553
496
|
},
|
|
554
497
|
})
|
|
555
498
|
);
|
|
556
499
|
await el.updateComplete;
|
|
557
500
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
501
|
+
const spy = Sinon.spy(el, 'browseHasExpired');
|
|
561
502
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
562
|
-
await aTimeout(100);
|
|
563
503
|
|
|
564
504
|
expect(spy.calledOnce).to.be.true;
|
|
565
505
|
});
|
|
566
506
|
});
|
|
567
507
|
|
|
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
|
-
|
|
718
508
|
describe('Shared Resize Observer', () => {
|
|
719
509
|
it('can receive a Shared Resize Observer', async () => {
|
|
720
510
|
const sharedObserverStub = new SharedResizeObserver();
|