@internetarchive/ia-book-actions 1.4.4-alpha2 → 1.4.4-alpha4
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 +1 -1
- package/src/ia-book-actions.js +47 -5
- package/test/ia-book-actions.test.js +197 -0
package/package.json
CHANGED
package/src/ia-book-actions.js
CHANGED
|
@@ -96,6 +96,15 @@ export default class IABookActions extends LitElement {
|
|
|
96
96
|
|
|
97
97
|
this.loanRenewInProgress = false;
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* True while a renewal in flight is recovering from a loan that
|
|
101
|
+
* genuinely lapsed (set by autoRenewExpiredLoan), as opposed to a
|
|
102
|
+
* routine pre-expiry top-up. Only the recovery case needs BookReader
|
|
103
|
+
* re-initialized once the renewal is confirmed — see
|
|
104
|
+
* handleLoanAutoRenewed.
|
|
105
|
+
*/
|
|
106
|
+
this.recoveringFromLoanExpiry = false;
|
|
107
|
+
|
|
99
108
|
this.warningModalOpen = false;
|
|
100
109
|
|
|
101
110
|
/**
|
|
@@ -290,10 +299,19 @@ export default class IABookActions extends LitElement {
|
|
|
290
299
|
|
|
291
300
|
/**
|
|
292
301
|
* tokenPoller determines if user has loan token for this book
|
|
293
|
-
* - if book is going to renew, need to wait until renew is completed
|
|
302
|
+
* - if book is going to renew, need to wait until renew is completed:
|
|
303
|
+
* loanRenewInProgress is set before renew_loan is even dispatched
|
|
304
|
+
* (including the optimistic pre-confirmation window) and only
|
|
305
|
+
* clears once the renewal is confirmed, so starting the poller here
|
|
306
|
+
* mid-renewal would call create_token against the loan record
|
|
307
|
+
* before it's actually renewed.
|
|
294
308
|
*/
|
|
295
309
|
setTimeout(() => {
|
|
296
|
-
if (
|
|
310
|
+
if (
|
|
311
|
+
!hasExpired &&
|
|
312
|
+
!this.loanRenewInProgress &&
|
|
313
|
+
!window.IALendingIntervals.tokenPoller
|
|
314
|
+
)
|
|
297
315
|
this.startLoanTokenPoller();
|
|
298
316
|
}, 100);
|
|
299
317
|
|
|
@@ -412,6 +430,7 @@ export default class IABookActions extends LitElement {
|
|
|
412
430
|
autoRenewExpiredLoan() {
|
|
413
431
|
if (this.loanRenewInProgress) return;
|
|
414
432
|
this.loanRenewInProgress = true;
|
|
433
|
+
this.recoveringFromLoanExpiry = true;
|
|
415
434
|
|
|
416
435
|
this.modal?.closeModal();
|
|
417
436
|
|
|
@@ -656,6 +675,7 @@ export default class IABookActions extends LitElement {
|
|
|
656
675
|
const activeLoan = detail?.data?.loan;
|
|
657
676
|
const errorMessage = `Whoops, seems we hit a hiccup with renewing this book. Please refresh & retry. --- (Debug: ${detail?.data?.error})`;
|
|
658
677
|
if (!activeLoan) {
|
|
678
|
+
this.recoveringFromLoanExpiry = false;
|
|
659
679
|
this.showErrorModal(errorMessage, 'handleLoanAutoRenewed');
|
|
660
680
|
return;
|
|
661
681
|
}
|
|
@@ -678,6 +698,18 @@ export default class IABookActions extends LitElement {
|
|
|
678
698
|
ajaxResponse: detail?.data,
|
|
679
699
|
});
|
|
680
700
|
|
|
701
|
+
if (this.recoveringFromLoanExpiry) {
|
|
702
|
+
// The loan genuinely lapsed, and renew_loan has now CONFIRMED it's
|
|
703
|
+
// renewed server-side — only now is it safe to let BookReader
|
|
704
|
+
// re-initialize (create_token was minting its access cookie off
|
|
705
|
+
// the loan's expiry; doing this before confirmation risked reading
|
|
706
|
+
// the still-expired record). loanRenewInProgress already kept the
|
|
707
|
+
// token poller from restarting early, so it'll pick up fresh here
|
|
708
|
+
// once lendingStatus below re-triggers setupLendingToolbarActions().
|
|
709
|
+
this.postInitComplete = false;
|
|
710
|
+
this.recoveringFromLoanExpiry = false;
|
|
711
|
+
}
|
|
712
|
+
|
|
681
713
|
const currStatus = {
|
|
682
714
|
...this.lendingStatus,
|
|
683
715
|
user_has_browsed: true,
|
|
@@ -899,14 +931,24 @@ export default class IABookActions extends LitElement {
|
|
|
899
931
|
handleLendingActionError(event) {
|
|
900
932
|
this.disableActionGroup = false;
|
|
901
933
|
|
|
902
|
-
// clear all intervals for lending system when error occured
|
|
903
|
-
window?.IALendingIntervals?.clearAll();
|
|
904
|
-
|
|
905
934
|
const action = event?.detail?.action;
|
|
906
935
|
const errorMsg = event?.detail?.data?.error;
|
|
907
936
|
|
|
937
|
+
// A create_token hiccup only affects BookReader's page-image access
|
|
938
|
+
// token — it says nothing about how much time is left on the loan
|
|
939
|
+
// itself, so it must not stop the reading countdown. Only clear the
|
|
940
|
+
// token poller so it can be retried. Every other action failure
|
|
941
|
+
// (renew_loan, browse_book, etc.) genuinely affects loan state, so it
|
|
942
|
+
// still clears everything as before.
|
|
943
|
+
if (action === 'create_token') {
|
|
944
|
+
window?.IALendingIntervals?.clearTokenPoller();
|
|
945
|
+
} else {
|
|
946
|
+
window?.IALendingIntervals?.clearAll();
|
|
947
|
+
}
|
|
948
|
+
|
|
908
949
|
if (action === 'renew_loan') {
|
|
909
950
|
this.loanRenewInProgress = false;
|
|
951
|
+
this.recoveringFromLoanExpiry = false;
|
|
910
952
|
}
|
|
911
953
|
|
|
912
954
|
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
@@ -689,6 +689,149 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
689
689
|
expect(el.loanRenewInProgress).to.be.false;
|
|
690
690
|
});
|
|
691
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(100);
|
|
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
|
+
|
|
692
835
|
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
693
836
|
const el = await fixture(
|
|
694
837
|
container({
|
|
@@ -783,6 +926,60 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
|
|
|
783
926
|
});
|
|
784
927
|
});
|
|
785
928
|
|
|
929
|
+
describe('handleLendingActionError - create_token failures must not stop the countdown (WEBDEV-8322 follow-up)', () => {
|
|
930
|
+
it('does not clear the reading countdown on a create_token failure', async () => {
|
|
931
|
+
const el = await fixture(
|
|
932
|
+
container({
|
|
933
|
+
userid: '@user1',
|
|
934
|
+
identifier: 'foobar',
|
|
935
|
+
lendingStatus: {
|
|
936
|
+
user_has_browsed: true,
|
|
937
|
+
browsingExpired: false,
|
|
938
|
+
secondsLeftOnLoan: 100,
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
);
|
|
942
|
+
await el.updateComplete;
|
|
943
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
944
|
+
|
|
945
|
+
el.handleLendingActionError({
|
|
946
|
+
detail: {
|
|
947
|
+
action: 'create_token',
|
|
948
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// The countdown must keep running — a token refresh hiccup says
|
|
953
|
+
// nothing about how much time is left on the loan itself.
|
|
954
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('still clears everything on a renew_loan failure', async () => {
|
|
958
|
+
const el = await fixture(
|
|
959
|
+
container({
|
|
960
|
+
userid: '@user1',
|
|
961
|
+
identifier: 'foobar',
|
|
962
|
+
lendingStatus: {
|
|
963
|
+
user_has_browsed: true,
|
|
964
|
+
browsingExpired: false,
|
|
965
|
+
secondsLeftOnLoan: 100,
|
|
966
|
+
},
|
|
967
|
+
})
|
|
968
|
+
);
|
|
969
|
+
await el.updateComplete;
|
|
970
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
971
|
+
|
|
972
|
+
el.handleLendingActionError({
|
|
973
|
+
detail: {
|
|
974
|
+
action: 'renew_loan',
|
|
975
|
+
data: { error: 'some other unrelated failure' },
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
|
|
786
983
|
describe('showWarningModal', () => {
|
|
787
984
|
it('shows a headline, one Okay button, and an info-icon help link; Okay dismisses without renewing', async () => {
|
|
788
985
|
const el = await fixture(
|