@internetarchive/ia-book-actions 1.4.3 → 1.4.4-alpha10
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/index.html +416 -118
- package/package.json +2 -2
- package/src/assets/data/info.js +12 -0
- package/src/core/services/actions-handler/actions-handler-service.js +5 -1
- package/src/core/services/actions-handler/actions-handler.js +25 -19
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +2 -3
- package/src/core/services/loan-token-poller.js +63 -16
- package/src/ia-book-actions.js +337 -247
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/core/services/loan-token-poller.test.js +76 -0
- package/test/ia-book-actions.test.js +716 -7
|
@@ -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', () => {
|
|
@@ -328,7 +349,9 @@ describe('Browsing expired status', () => {
|
|
|
328
349
|
await aTimeout(1500); // wait for 1.5 second
|
|
329
350
|
await el.updateComplete;
|
|
330
351
|
|
|
331
|
-
|
|
352
|
+
// Auto-return must not visibly change the action bar — it should stay
|
|
353
|
+
// exactly as it looked while reading (see WEBDEV-8322).
|
|
354
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
332
355
|
|
|
333
356
|
expect(el.timerCountdownEl).to.exist;
|
|
334
357
|
|
|
@@ -371,7 +394,9 @@ describe('Browsing expired status', () => {
|
|
|
371
394
|
await aTimeout(1500); // wait for 1.5 sec
|
|
372
395
|
|
|
373
396
|
expect(eventReceived).to.equal(true);
|
|
374
|
-
|
|
397
|
+
// Auto-return must not visibly change the action bar — it should stay
|
|
398
|
+
// exactly as it looked while reading (see WEBDEV-8322).
|
|
399
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
375
400
|
expect(el.tokenPoller.loanTokenInterval).to.equal(undefined);
|
|
376
401
|
});
|
|
377
402
|
});
|
|
@@ -467,7 +492,7 @@ describe('Visibility change API for document', async () => {
|
|
|
467
492
|
});
|
|
468
493
|
});
|
|
469
494
|
|
|
470
|
-
it('when book is not expired', async () => {
|
|
495
|
+
it('when book is not expired and loan time is valid', async () => {
|
|
471
496
|
const el = await fixture(
|
|
472
497
|
container({
|
|
473
498
|
userid: '@user1',
|
|
@@ -480,29 +505,713 @@ describe('Visibility change API for document', async () => {
|
|
|
480
505
|
);
|
|
481
506
|
await el.updateComplete;
|
|
482
507
|
|
|
508
|
+
// Set a valid loan time well into the future
|
|
509
|
+
await el.localCache.set({
|
|
510
|
+
key: 'foobar-loanTime',
|
|
511
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
512
|
+
ttl: 3600,
|
|
513
|
+
});
|
|
514
|
+
|
|
483
515
|
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
484
516
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
517
|
+
await aTimeout(100);
|
|
485
518
|
|
|
486
519
|
expect(spy.calledOnce).to.be.true;
|
|
487
520
|
});
|
|
488
521
|
|
|
489
|
-
it('when
|
|
522
|
+
it('when loan expired while tab was hidden (browsingExpired false, loanTime past)', async () => {
|
|
490
523
|
const el = await fixture(
|
|
491
524
|
container({
|
|
492
525
|
userid: '@user1',
|
|
493
526
|
identifier: 'foobar',
|
|
494
527
|
lendingStatus: {
|
|
495
528
|
user_has_browsed: true,
|
|
529
|
+
browsingExpired: false,
|
|
496
530
|
},
|
|
497
531
|
})
|
|
498
532
|
);
|
|
499
533
|
await el.updateComplete;
|
|
500
534
|
|
|
501
|
-
|
|
535
|
+
// Set a loan time already in the past
|
|
536
|
+
await el.localCache.set({
|
|
537
|
+
key: 'foobar-loanTime',
|
|
538
|
+
value: new Date(new Date().getTime() - 10 * 1000),
|
|
539
|
+
ttl: 3600,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
502
543
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
544
|
+
await aTimeout(100);
|
|
503
545
|
|
|
504
546
|
expect(spy.calledOnce).to.be.true;
|
|
505
547
|
});
|
|
548
|
+
|
|
549
|
+
it('when loan already marked expired (browsingExpired true)', async () => {
|
|
550
|
+
const el = await fixture(
|
|
551
|
+
container({
|
|
552
|
+
userid: '@user1',
|
|
553
|
+
identifier: 'foobar',
|
|
554
|
+
lendingStatus: {
|
|
555
|
+
user_has_browsed: true,
|
|
556
|
+
browsingExpired: true,
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
await el.updateComplete;
|
|
561
|
+
|
|
562
|
+
el.borrowType = 'browsed';
|
|
563
|
+
|
|
564
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
565
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
566
|
+
await aTimeout(100);
|
|
567
|
+
|
|
568
|
+
expect(spy.calledOnce).to.be.true;
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('autoRenewExpiredLoan', () => {
|
|
573
|
+
it('optimistically resets browsingExpired so the button stays red', async () => {
|
|
574
|
+
const el = await fixture(
|
|
575
|
+
container({
|
|
576
|
+
userid: '@user1',
|
|
577
|
+
identifier: 'foobar',
|
|
578
|
+
lendingStatus: {
|
|
579
|
+
user_has_browsed: true,
|
|
580
|
+
available_to_browse: true,
|
|
581
|
+
browsingExpired: true,
|
|
582
|
+
},
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
await el.updateComplete;
|
|
586
|
+
|
|
587
|
+
// Before: browsingExpired true → borrow1HrAction → primary color is 'primary' (blue)
|
|
588
|
+
expect(el.primaryColor).to.equal('primary');
|
|
589
|
+
|
|
590
|
+
el.autoRenewExpiredLoan();
|
|
591
|
+
await el.updateComplete;
|
|
592
|
+
|
|
593
|
+
// After: browsingExpired flipped to false → patronIsReadingAction → 'danger' (red)
|
|
594
|
+
expect(el.lendingStatus.browsingExpired).to.be.false;
|
|
595
|
+
expect(el.primaryColor).to.equal('danger');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
|
|
599
|
+
const el = await fixture(
|
|
600
|
+
container({
|
|
601
|
+
userid: '@user1',
|
|
602
|
+
identifier: 'foobar',
|
|
603
|
+
lendingStatus: {
|
|
604
|
+
user_has_browsed: true,
|
|
605
|
+
browsingExpired: true,
|
|
606
|
+
},
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
await el.updateComplete;
|
|
610
|
+
|
|
611
|
+
el.autoRenewExpiredLoan();
|
|
612
|
+
|
|
613
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
614
|
+
|
|
615
|
+
// renewNow is set in a setTimeout — flush the macrotask queue
|
|
616
|
+
await aTimeout(50);
|
|
617
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
618
|
+
expect(el.loanRenewResult.renewType).to.equal('auto');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('ignores subsequent calls while a renewal is already in progress', async () => {
|
|
622
|
+
const el = await fixture(
|
|
623
|
+
container({
|
|
624
|
+
userid: '@user1',
|
|
625
|
+
identifier: 'foobar',
|
|
626
|
+
lendingStatus: {
|
|
627
|
+
user_has_browsed: true,
|
|
628
|
+
browsingExpired: true,
|
|
629
|
+
},
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
await el.updateComplete;
|
|
633
|
+
|
|
634
|
+
// Spy *after* fixture so the handler is the same instance
|
|
635
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
636
|
+
|
|
637
|
+
el.autoRenewExpiredLoan(); // first call — proceeds
|
|
638
|
+
el.autoRenewExpiredLoan(); // second call — should be a no-op
|
|
639
|
+
|
|
640
|
+
// The spy counts both invocations but the internal guard stops the second
|
|
641
|
+
expect(spy.callCount).to.equal(2);
|
|
642
|
+
// loanRenewInProgress is still true (not double-set or cleared)
|
|
643
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
644
|
+
|
|
645
|
+
// Only one setTimeout fires (the second call returned early)
|
|
646
|
+
await aTimeout(50);
|
|
647
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
|
|
651
|
+
const el = await fixture(
|
|
652
|
+
container({
|
|
653
|
+
userid: '@user1',
|
|
654
|
+
identifier: 'foobar',
|
|
655
|
+
lendingStatus: {
|
|
656
|
+
user_has_browsed: true,
|
|
657
|
+
browsingExpired: false,
|
|
658
|
+
secondsLeftOnLoan: 100,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
await el.updateComplete;
|
|
663
|
+
|
|
664
|
+
await el.localCache.set({
|
|
665
|
+
key: 'foobar-loanTime',
|
|
666
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
667
|
+
ttl: 3600,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
el.autoRenewExpiredLoan();
|
|
671
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
672
|
+
|
|
673
|
+
// Wait for the deferred renewNow to be set
|
|
674
|
+
await aTimeout(50);
|
|
675
|
+
|
|
676
|
+
// Simulate the renew_loan success response
|
|
677
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
678
|
+
'collapsible-action-group'
|
|
679
|
+
);
|
|
680
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
681
|
+
new CustomEvent('loanAutoRenewed', {
|
|
682
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
await aTimeout(100);
|
|
687
|
+
await el.updateComplete;
|
|
688
|
+
|
|
689
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('does not reset postInitComplete until renewal is confirmed, not on the optimistic flip (WEBDEV-8322 follow-up)', async () => {
|
|
693
|
+
// Regression: resetting postInitComplete (which lets BookReader
|
|
694
|
+
// re-initialize via create_token) as soon as autoRenewExpiredLoan()
|
|
695
|
+
// optimistically flips browsingExpired would let create_token race
|
|
696
|
+
// ahead of renew_loan's response, reading the still-expired loan
|
|
697
|
+
// record and minting a bad access cookie — breaking page images with
|
|
698
|
+
// CORS/auth errors until the next natural token refresh.
|
|
699
|
+
const el = await fixture(
|
|
700
|
+
container({
|
|
701
|
+
userid: '@user1',
|
|
702
|
+
identifier: 'foobar',
|
|
703
|
+
lendingStatus: {
|
|
704
|
+
user_has_browsed: true,
|
|
705
|
+
browsingExpired: true,
|
|
706
|
+
},
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
await el.updateComplete;
|
|
710
|
+
el.postInitComplete = true;
|
|
711
|
+
el.lendingBarPostInit = Sinon.spy();
|
|
712
|
+
|
|
713
|
+
await el.localCache.set({
|
|
714
|
+
key: 'foobar-loanTime',
|
|
715
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
716
|
+
ttl: 3600,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
el.autoRenewExpiredLoan();
|
|
720
|
+
await el.updateComplete;
|
|
721
|
+
|
|
722
|
+
// Still true right after the optimistic flip — must not reset early.
|
|
723
|
+
expect(el.postInitComplete).to.be.true;
|
|
724
|
+
expect(el.lendingBarPostInit.called).to.be.false;
|
|
725
|
+
|
|
726
|
+
await aTimeout(50);
|
|
727
|
+
|
|
728
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
729
|
+
'collapsible-action-group'
|
|
730
|
+
);
|
|
731
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
732
|
+
new CustomEvent('loanAutoRenewed', {
|
|
733
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
734
|
+
})
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
await aTimeout(200);
|
|
738
|
+
await el.updateComplete;
|
|
739
|
+
|
|
740
|
+
// The confirmed renewal must have forced a fresh create_token cycle
|
|
741
|
+
// that re-ran lendingBarPostInit() (BookReader re-init) — postInitComplete
|
|
742
|
+
// itself ends up true again once that cycle completes, so we assert on
|
|
743
|
+
// the re-init actually having fired instead of the transient flag.
|
|
744
|
+
expect(el.lendingBarPostInit.calledOnce).to.be.true;
|
|
745
|
+
expect(el.recoveringFromLoanExpiry).to.be.false;
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('does not start the token poller (create_token) on the optimistic flip, only after renewal is confirmed', async () => {
|
|
749
|
+
// Regression: even without re-running lendingBarPostInit early, the
|
|
750
|
+
// token poller restart in setupLendingToolbarActions() was ungated —
|
|
751
|
+
// it fired on the optimistic lendingStatus flip too, calling
|
|
752
|
+
// create_token against the still-expired loan. When that failed,
|
|
753
|
+
// handleLendingActionError cleared ALL lending intervals (including
|
|
754
|
+
// the countdown timer) and flipped user_has_browsed false, sometimes
|
|
755
|
+
// permanently killing the countdown if it lost the race against the
|
|
756
|
+
// real renewal confirming afterward.
|
|
757
|
+
const el = await fixture(
|
|
758
|
+
container({
|
|
759
|
+
userid: '@user1',
|
|
760
|
+
identifier: 'foobar',
|
|
761
|
+
lendingStatus: {
|
|
762
|
+
user_has_browsed: true,
|
|
763
|
+
browsingExpired: true,
|
|
764
|
+
},
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
await el.updateComplete;
|
|
768
|
+
|
|
769
|
+
await el.localCache.set({
|
|
770
|
+
key: 'foobar-loanTime',
|
|
771
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
772
|
+
ttl: 3600,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
776
|
+
|
|
777
|
+
el.autoRenewExpiredLoan();
|
|
778
|
+
await el.updateComplete;
|
|
779
|
+
await aTimeout(150); // let the 100ms token-poller-restart check run
|
|
780
|
+
|
|
781
|
+
expect(startTokenPollerSpy.called).to.be.false;
|
|
782
|
+
|
|
783
|
+
await aTimeout(50);
|
|
784
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
785
|
+
'collapsible-action-group'
|
|
786
|
+
);
|
|
787
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
788
|
+
new CustomEvent('loanAutoRenewed', {
|
|
789
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
790
|
+
})
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
await aTimeout(200);
|
|
794
|
+
await el.updateComplete;
|
|
795
|
+
|
|
796
|
+
expect(startTokenPollerSpy.calledOnce).to.be.true;
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('does not touch postInitComplete on a routine (non-expiry) renewal', async () => {
|
|
800
|
+
// A background top-up renewal (loan never actually lapsed) never
|
|
801
|
+
// interrupted BookReader, so it must not force a re-init.
|
|
802
|
+
const el = await fixture(
|
|
803
|
+
container({
|
|
804
|
+
userid: '@user1',
|
|
805
|
+
identifier: 'foobar',
|
|
806
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: false },
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
await el.updateComplete;
|
|
810
|
+
el.postInitComplete = true;
|
|
811
|
+
el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
812
|
+
el.lendingBarPostInit = Sinon.spy();
|
|
813
|
+
|
|
814
|
+
await el.localCache.set({
|
|
815
|
+
key: 'foobar-loanTime',
|
|
816
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
817
|
+
ttl: 3600,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
821
|
+
'collapsible-action-group'
|
|
822
|
+
);
|
|
823
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
824
|
+
new CustomEvent('loanAutoRenewed', {
|
|
825
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
826
|
+
})
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
await aTimeout(100);
|
|
830
|
+
await el.updateComplete;
|
|
831
|
+
|
|
832
|
+
expect(el.lendingBarPostInit.called).to.be.false;
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
836
|
+
const el = await fixture(
|
|
837
|
+
container({
|
|
838
|
+
userid: '@user1',
|
|
839
|
+
identifier: 'foobar',
|
|
840
|
+
lendingStatus: {
|
|
841
|
+
user_has_browsed: true,
|
|
842
|
+
browsingExpired: true,
|
|
843
|
+
},
|
|
844
|
+
})
|
|
845
|
+
);
|
|
846
|
+
await el.updateComplete;
|
|
847
|
+
|
|
848
|
+
const showModalSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
849
|
+
|
|
850
|
+
el.autoRenewExpiredLoan();
|
|
851
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
852
|
+
|
|
853
|
+
el.handleLendingActionError({
|
|
854
|
+
detail: {
|
|
855
|
+
action: 'renew_loan',
|
|
856
|
+
data: { error: 'book is not available' },
|
|
857
|
+
},
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
861
|
+
expect(showModalSpy.calledOnce).to.be.true;
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
describe('BookReader:userAction race regression (WEBDEV-8322)', () => {
|
|
866
|
+
it('does not let autoLoanRenewChecker clobber an in-flight expired-loan renewal', async () => {
|
|
867
|
+
// Get borrowType derived as 'browsed' first, same as the
|
|
868
|
+
// "Expiring book cancels interval" test above — hasExpired renders
|
|
869
|
+
// leave borrowType untouched, so it must already be 'browsed' before
|
|
870
|
+
// the loan is marked expired.
|
|
871
|
+
const baseStatus = {
|
|
872
|
+
is_lendable: true,
|
|
873
|
+
user_has_browsed: true,
|
|
874
|
+
browsingExpired: false,
|
|
875
|
+
};
|
|
876
|
+
const el = await fixture(
|
|
877
|
+
container({
|
|
878
|
+
userid: '@user1',
|
|
879
|
+
identifier: 'foobar',
|
|
880
|
+
lendingStatus: baseStatus,
|
|
881
|
+
})
|
|
882
|
+
);
|
|
883
|
+
await el.updateComplete;
|
|
884
|
+
expect(el.borrowType).to.equal('browsed');
|
|
885
|
+
|
|
886
|
+
el.lendingStatus = { ...baseStatus, browsingExpired: true };
|
|
887
|
+
await el.updateComplete;
|
|
888
|
+
|
|
889
|
+
const autoLoanRenewCheckerSpy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
890
|
+
|
|
891
|
+
window.dispatchEvent(new CustomEvent('BookReader:userAction'));
|
|
892
|
+
|
|
893
|
+
// autoRenewExpiredLoan()'s setTimeout(0) sets renewNow=true. If the old
|
|
894
|
+
// bug were present, the still-live lendingStatus.browsingExpired check
|
|
895
|
+
// would (wrongly) also let autoLoanRenewChecker(true) run — which reads
|
|
896
|
+
// the already-deleted loanTime cache key and overwrites renewNow back
|
|
897
|
+
// to false. Give both paths plenty of time to resolve.
|
|
898
|
+
await aTimeout(300);
|
|
899
|
+
|
|
900
|
+
expect(autoLoanRenewCheckerSpy.called).to.be.false;
|
|
901
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('ignores repeated BookReader:userAction events firing in quick succession (e.g. from a scroll)', async () => {
|
|
905
|
+
// Regression: a single scroll gesture can fire BookReader:userAction
|
|
906
|
+
// several times in rapid succession. autoLoanRenewChecker() had no
|
|
907
|
+
// guard against concurrent/repeated invocation, unlike
|
|
908
|
+
// autoRenewExpiredLoan() — each event spun up its own LoanRenewHelper,
|
|
909
|
+
// racing to overwrite this.loanRenewResult and potentially
|
|
910
|
+
// re-triggering renew_loan/create_token while a prior call from an
|
|
911
|
+
// earlier event was still in flight or had just landed.
|
|
912
|
+
const baseStatus = {
|
|
913
|
+
is_lendable: true,
|
|
914
|
+
user_has_browsed: true,
|
|
915
|
+
browsingExpired: false,
|
|
916
|
+
};
|
|
917
|
+
const el = await fixture(
|
|
918
|
+
container({
|
|
919
|
+
userid: '@user1',
|
|
920
|
+
identifier: 'foobar',
|
|
921
|
+
lendingStatus: baseStatus,
|
|
922
|
+
})
|
|
923
|
+
);
|
|
924
|
+
await el.updateComplete;
|
|
925
|
+
expect(el.borrowType).to.equal('browsed');
|
|
926
|
+
|
|
927
|
+
el.loanRenewInProgress = true;
|
|
928
|
+
|
|
929
|
+
const loanRenewHelperSpy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
930
|
+
|
|
931
|
+
// Simulate a scroll firing the event several times back to back.
|
|
932
|
+
window.dispatchEvent(new CustomEvent('BookReader:userAction'));
|
|
933
|
+
window.dispatchEvent(new CustomEvent('BookReader:userAction'));
|
|
934
|
+
window.dispatchEvent(new CustomEvent('BookReader:userAction'));
|
|
935
|
+
await aTimeout(50);
|
|
936
|
+
|
|
937
|
+
// The method is still called each time (the guard is inside it), but
|
|
938
|
+
// must no-op instead of spinning up a new LoanRenewHelper each time.
|
|
939
|
+
expect(loanRenewHelperSpy.callCount).to.equal(3);
|
|
940
|
+
expect(el.loanRenewHelper).to.be.undefined;
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe('handleLendingActionError - loanRenewInProgress reset', () => {
|
|
945
|
+
it('clears loanRenewInProgress on any renew_loan failure, not just "not available"', async () => {
|
|
946
|
+
const el = await fixture(
|
|
947
|
+
container({
|
|
948
|
+
userid: '@user1',
|
|
949
|
+
identifier: 'foobar',
|
|
950
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
await el.updateComplete;
|
|
954
|
+
|
|
955
|
+
el.loanRenewInProgress = true;
|
|
956
|
+
|
|
957
|
+
el.handleLendingActionError({
|
|
958
|
+
detail: {
|
|
959
|
+
action: 'renew_loan',
|
|
960
|
+
data: { error: 'some other unrelated failure' },
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
describe('handleLendingActionError - create_token failures must not stop the countdown (WEBDEV-8322 follow-up)', () => {
|
|
969
|
+
it('does not clear the reading countdown on a create_token failure', async () => {
|
|
970
|
+
const el = await fixture(
|
|
971
|
+
container({
|
|
972
|
+
userid: '@user1',
|
|
973
|
+
identifier: 'foobar',
|
|
974
|
+
lendingStatus: {
|
|
975
|
+
user_has_browsed: true,
|
|
976
|
+
browsingExpired: false,
|
|
977
|
+
secondsLeftOnLoan: 100,
|
|
978
|
+
},
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
await el.updateComplete;
|
|
982
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
983
|
+
|
|
984
|
+
el.handleLendingActionError({
|
|
985
|
+
detail: {
|
|
986
|
+
action: 'create_token',
|
|
987
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// The countdown must keep running — a token refresh hiccup says
|
|
992
|
+
// nothing about how much time is left on the loan itself.
|
|
993
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('shows the error modal for a create_token failure that reaches here (retries already exhausted)', async () => {
|
|
997
|
+
// By the time handleLendingActionError runs for a create_token
|
|
998
|
+
// failure, LoanTokenPoller has already retried and given up (see
|
|
999
|
+
// loan-token-poller.js's handleTokenError) — this is a genuine
|
|
1000
|
+
// failure, so the patron must be told instead of it happening
|
|
1001
|
+
// silently.
|
|
1002
|
+
const el = await fixture(
|
|
1003
|
+
container({
|
|
1004
|
+
userid: '@user1',
|
|
1005
|
+
identifier: 'foobar',
|
|
1006
|
+
lendingStatus: {
|
|
1007
|
+
user_has_browsed: true,
|
|
1008
|
+
browsingExpired: false,
|
|
1009
|
+
secondsLeftOnLoan: 100,
|
|
1010
|
+
},
|
|
1011
|
+
})
|
|
1012
|
+
);
|
|
1013
|
+
await el.updateComplete;
|
|
1014
|
+
|
|
1015
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1016
|
+
const errorMsg = 'loan token not found. please try again later.';
|
|
1017
|
+
|
|
1018
|
+
el.handleLendingActionError({
|
|
1019
|
+
detail: {
|
|
1020
|
+
action: 'create_token',
|
|
1021
|
+
data: { error: errorMsg },
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
|
|
1026
|
+
.true;
|
|
1027
|
+
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('still clears everything on a renew_loan failure', async () => {
|
|
1031
|
+
const el = await fixture(
|
|
1032
|
+
container({
|
|
1033
|
+
userid: '@user1',
|
|
1034
|
+
identifier: 'foobar',
|
|
1035
|
+
lendingStatus: {
|
|
1036
|
+
user_has_browsed: true,
|
|
1037
|
+
browsingExpired: false,
|
|
1038
|
+
secondsLeftOnLoan: 100,
|
|
1039
|
+
},
|
|
1040
|
+
})
|
|
1041
|
+
);
|
|
1042
|
+
await el.updateComplete;
|
|
1043
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1044
|
+
|
|
1045
|
+
el.handleLendingActionError({
|
|
1046
|
+
detail: {
|
|
1047
|
+
action: 'renew_loan',
|
|
1048
|
+
data: { error: 'some other unrelated failure' },
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('shows the same unavailable-modal-and-refresh path for any renew_loan failure, regardless of message text', async () => {
|
|
1056
|
+
// Regression: branching on errorMsg.match(/not available/) to decide
|
|
1057
|
+
// how to recover was fragile — the server can return any message
|
|
1058
|
+
// (e.g. a lending limit hit), and guessing which lendingStatus fields
|
|
1059
|
+
// are now accurate client-side is error-prone. Every renew_loan
|
|
1060
|
+
// failure now shows the real error and refreshes the page on
|
|
1061
|
+
// dismissal (showLoanUnavailableModal's Okay button), rather than
|
|
1062
|
+
// trying to patch up state without reloading from the server.
|
|
1063
|
+
const el = await fixture(
|
|
1064
|
+
container({
|
|
1065
|
+
userid: '@user1',
|
|
1066
|
+
identifier: 'foobar',
|
|
1067
|
+
lendingStatus: {
|
|
1068
|
+
is_lendable: true,
|
|
1069
|
+
user_has_browsed: true,
|
|
1070
|
+
browsingExpired: false,
|
|
1071
|
+
available_to_browse: false,
|
|
1072
|
+
secondsLeftOnLoan: 100,
|
|
1073
|
+
},
|
|
1074
|
+
})
|
|
1075
|
+
);
|
|
1076
|
+
await el.updateComplete;
|
|
1077
|
+
|
|
1078
|
+
const showUnavailableSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
1079
|
+
const lendingLimitMsg =
|
|
1080
|
+
'Your account has hit a lending limit. Please try again later or contact info@archive.org.';
|
|
1081
|
+
|
|
1082
|
+
el.handleLendingActionError({
|
|
1083
|
+
detail: {
|
|
1084
|
+
action: 'renew_loan',
|
|
1085
|
+
data: { error: lendingLimitMsg },
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
await el.updateComplete;
|
|
1089
|
+
|
|
1090
|
+
expect(showUnavailableSpy.calledOnceWith(lendingLimitMsg)).to.be.true;
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it('updates lendingStatus immediately on a renew_loan failure — Borrow state, timer cleared', async () => {
|
|
1094
|
+
// The modal's Okay button refreshes the page, but that shouldn't be
|
|
1095
|
+
// the only thing correcting the UI — while the modal is still open,
|
|
1096
|
+
// the action bar must already show Borrow instead of "Return now"
|
|
1097
|
+
// with a stale countdown value from before the failed renewal.
|
|
1098
|
+
const el = await fixture(
|
|
1099
|
+
container({
|
|
1100
|
+
userid: '@user1',
|
|
1101
|
+
identifier: 'foobar',
|
|
1102
|
+
lendingStatus: {
|
|
1103
|
+
is_lendable: true,
|
|
1104
|
+
user_has_browsed: true,
|
|
1105
|
+
browsingExpired: false,
|
|
1106
|
+
available_to_browse: false,
|
|
1107
|
+
secondsLeftOnLoan: 100,
|
|
1108
|
+
},
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
await el.updateComplete;
|
|
1112
|
+
|
|
1113
|
+
el.handleLendingActionError({
|
|
1114
|
+
detail: {
|
|
1115
|
+
action: 'renew_loan',
|
|
1116
|
+
data: {
|
|
1117
|
+
error:
|
|
1118
|
+
'Your account has hit a lending limit. Please try again later or contact info@archive.org.',
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
await el.updateComplete;
|
|
1123
|
+
|
|
1124
|
+
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1125
|
+
expect(el.lendingStatus.available_to_browse).to.be.true;
|
|
1126
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1127
|
+
expect(el.primaryActions[0].text).to.not.equal('Return now');
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('also shows the unavailable modal for the "not available" message, with its own text', async () => {
|
|
1131
|
+
const el = await fixture(
|
|
1132
|
+
container({
|
|
1133
|
+
userid: '@user1',
|
|
1134
|
+
identifier: 'foobar',
|
|
1135
|
+
lendingStatus: {
|
|
1136
|
+
is_lendable: true,
|
|
1137
|
+
user_has_browsed: true,
|
|
1138
|
+
browsingExpired: false,
|
|
1139
|
+
secondsLeftOnLoan: 100,
|
|
1140
|
+
},
|
|
1141
|
+
})
|
|
1142
|
+
);
|
|
1143
|
+
await el.updateComplete;
|
|
1144
|
+
|
|
1145
|
+
const showUnavailableSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
1146
|
+
const notAvailableMsg = 'This book is not available to borrow at this time.';
|
|
1147
|
+
|
|
1148
|
+
el.handleLendingActionError({
|
|
1149
|
+
detail: {
|
|
1150
|
+
action: 'renew_loan',
|
|
1151
|
+
data: { error: notAvailableMsg },
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1154
|
+
await el.updateComplete;
|
|
1155
|
+
|
|
1156
|
+
expect(showUnavailableSpy.calledOnceWith(notAvailableMsg)).to.be.true;
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
describe('showWarningModal', () => {
|
|
1161
|
+
it('shows a headline, one Okay button, and an info-icon help link; Okay dismisses without renewing', async () => {
|
|
1162
|
+
const el = await fixture(
|
|
1163
|
+
container({
|
|
1164
|
+
userid: '@user1',
|
|
1165
|
+
identifier: 'foobar',
|
|
1166
|
+
lendingStatus: { user_has_browsed: true, secondsLeftOnLoan: 100 },
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
await el.updateComplete;
|
|
1170
|
+
|
|
1171
|
+
await el.showWarningModal();
|
|
1172
|
+
|
|
1173
|
+
const modalManagerEl = document.body.querySelector('modal-manager');
|
|
1174
|
+
const modalTemplateEl =
|
|
1175
|
+
modalManagerEl.shadowRoot.querySelector('modal-template');
|
|
1176
|
+
const headline =
|
|
1177
|
+
modalTemplateEl.shadowRoot.querySelector('.headline')?.textContent;
|
|
1178
|
+
expect(headline).to.contain('Are you still there?');
|
|
1179
|
+
|
|
1180
|
+
const helpLink = modalTemplateEl.shadowRoot.querySelector(
|
|
1181
|
+
'a[href="https://help.archive.org/help/borrowing-from-the-lending-library"]'
|
|
1182
|
+
);
|
|
1183
|
+
expect(helpLink).to.exist;
|
|
1184
|
+
|
|
1185
|
+
const buttons = modalManagerEl.shadowRoot.querySelectorAll(
|
|
1186
|
+
'#book-action-bar-custom-buttons button'
|
|
1187
|
+
);
|
|
1188
|
+
expect(buttons.length).to.equal(1);
|
|
1189
|
+
expect(buttons[0].textContent).to.contain('Okay');
|
|
1190
|
+
|
|
1191
|
+
buttons[0].click();
|
|
1192
|
+
|
|
1193
|
+
expect(el.warningModalOpen).to.be.false;
|
|
1194
|
+
expect(el.warningModalDismissed).to.be.true;
|
|
1195
|
+
expect(el.loanRenewResult.renewNow).to.be.false;
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it('does not reopen while already open', async () => {
|
|
1199
|
+
const el = await fixture(
|
|
1200
|
+
container({
|
|
1201
|
+
userid: '@user1',
|
|
1202
|
+
identifier: 'foobar',
|
|
1203
|
+
lendingStatus: { user_has_browsed: true, secondsLeftOnLoan: 100 },
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
await el.updateComplete;
|
|
1207
|
+
|
|
1208
|
+
await el.showWarningModal();
|
|
1209
|
+
const showModalSpy = Sinon.spy(el.modal, 'showModal');
|
|
1210
|
+
|
|
1211
|
+
await el.showWarningModal();
|
|
1212
|
+
|
|
1213
|
+
expect(showModalSpy.called).to.be.false;
|
|
1214
|
+
});
|
|
506
1215
|
});
|
|
507
1216
|
|
|
508
1217
|
describe('Shared Resize Observer', () => {
|