@internetarchive/ia-book-actions 1.4.4-alpha3 → 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 -15
- 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,19 +430,10 @@ 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
|
|
|
418
|
-
// The loan genuinely lapsed while the tab was hidden/backgrounded, so
|
|
419
|
-
// BookReader's page-image access (issued via create_token, not
|
|
420
|
-
// renew_loan) died with it. Resetting this lets the token poller's
|
|
421
|
-
// next successful create_token call re-run lendingBarPostInit() —
|
|
422
|
-
// whatever re-initializes BookReader (e.g. br.init()) — instead of
|
|
423
|
-
// silently skipping it because a token was already issued once this
|
|
424
|
-
// page load. Without this, the loan's countdown resets but BookReader
|
|
425
|
-
// keeps using its now-dead session, so page images break.
|
|
426
|
-
this.postInitComplete = false;
|
|
427
|
-
|
|
428
437
|
// Optimistically flip browsingExpired back to false so the action bar
|
|
429
438
|
// stays red (patronIsReadingAction) while the renew_loan request is in
|
|
430
439
|
// flight, instead of showing the blue "Borrow" state.
|
|
@@ -666,6 +675,7 @@ export default class IABookActions extends LitElement {
|
|
|
666
675
|
const activeLoan = detail?.data?.loan;
|
|
667
676
|
const errorMessage = `Whoops, seems we hit a hiccup with renewing this book. Please refresh & retry. --- (Debug: ${detail?.data?.error})`;
|
|
668
677
|
if (!activeLoan) {
|
|
678
|
+
this.recoveringFromLoanExpiry = false;
|
|
669
679
|
this.showErrorModal(errorMessage, 'handleLoanAutoRenewed');
|
|
670
680
|
return;
|
|
671
681
|
}
|
|
@@ -688,6 +698,18 @@ export default class IABookActions extends LitElement {
|
|
|
688
698
|
ajaxResponse: detail?.data,
|
|
689
699
|
});
|
|
690
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
|
+
|
|
691
713
|
const currStatus = {
|
|
692
714
|
...this.lendingStatus,
|
|
693
715
|
user_has_browsed: true,
|
|
@@ -909,14 +931,24 @@ export default class IABookActions extends LitElement {
|
|
|
909
931
|
handleLendingActionError(event) {
|
|
910
932
|
this.disableActionGroup = false;
|
|
911
933
|
|
|
912
|
-
// clear all intervals for lending system when error occured
|
|
913
|
-
window?.IALendingIntervals?.clearAll();
|
|
914
|
-
|
|
915
934
|
const action = event?.detail?.action;
|
|
916
935
|
const errorMsg = event?.detail?.data?.error;
|
|
917
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
|
+
|
|
918
949
|
if (action === 'renew_loan') {
|
|
919
950
|
this.loanRenewInProgress = false;
|
|
951
|
+
this.recoveringFromLoanExpiry = false;
|
|
920
952
|
}
|
|
921
953
|
|
|
922
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(
|