@internetarchive/ia-book-actions 1.4.4-alpha4 → 1.4.4-alpha6
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 +54 -43
- package/test/ia-book-actions.test.js +143 -0
package/package.json
CHANGED
package/src/ia-book-actions.js
CHANGED
|
@@ -407,6 +407,16 @@ export default class IABookActions extends LitElement {
|
|
|
407
407
|
* @param {Boolean} hasPageChanged
|
|
408
408
|
*/
|
|
409
409
|
async autoLoanRenewChecker(hasPageChanged = false) {
|
|
410
|
+
// Guards against re-entrancy the same way autoRenewExpiredLoan() does.
|
|
411
|
+
// BookReader:userAction can fire several times in quick succession
|
|
412
|
+
// (e.g. a single scroll gesture), and this method has no debouncing of
|
|
413
|
+
// its own — without this guard, each event would spin up its own
|
|
414
|
+
// LoanRenewHelper concurrently, and whichever's async localCache reads
|
|
415
|
+
// resolve last would clobber this.loanRenewResult, potentially
|
|
416
|
+
// re-triggering the whole renew_loan/create_token flow while a
|
|
417
|
+
// renewal from an earlier event is already in flight or just landed.
|
|
418
|
+
if (this.loanRenewInProgress) return;
|
|
419
|
+
|
|
410
420
|
this.loanRenewHelper = new LoanRenewHelper(
|
|
411
421
|
hasPageChanged,
|
|
412
422
|
this.identifier,
|
|
@@ -568,13 +578,14 @@ export default class IABookActions extends LitElement {
|
|
|
568
578
|
* Show modal when the book can no longer be automatically renewed
|
|
569
579
|
* because another patron has checked it out.
|
|
570
580
|
*/
|
|
571
|
-
async showLoanUnavailableModal() {
|
|
581
|
+
async showLoanUnavailableModal(errorMsg) {
|
|
572
582
|
const config = new ModalConfig({
|
|
573
583
|
headline: '',
|
|
574
584
|
showCloseButton: false,
|
|
575
585
|
closeOnBackdropClick: false,
|
|
576
586
|
headerColor: '#d9534f',
|
|
577
587
|
message:
|
|
588
|
+
errorMsg ||
|
|
578
589
|
'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
|
|
579
590
|
});
|
|
580
591
|
|
|
@@ -934,60 +945,60 @@ export default class IABookActions extends LitElement {
|
|
|
934
945
|
const action = event?.detail?.action;
|
|
935
946
|
const errorMsg = event?.detail?.data?.error;
|
|
936
947
|
|
|
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
948
|
if (action === 'create_token') {
|
|
949
|
+
// A create_token hiccup only affects BookReader's page-image access
|
|
950
|
+
// token — it says nothing about how much time is left on the loan
|
|
951
|
+
// itself, so it must not stop the reading countdown. Only clear the
|
|
952
|
+
// token poller so it can be retried.
|
|
944
953
|
window?.IALendingIntervals?.clearTokenPoller();
|
|
945
|
-
} else {
|
|
946
|
-
window?.IALendingIntervals?.clearAll();
|
|
947
|
-
}
|
|
948
954
|
|
|
949
|
-
|
|
955
|
+
// The loan itself is fine — just this token refresh failed — but
|
|
956
|
+
// the action bar must reflect that it isn't safely browsable right
|
|
957
|
+
// now rather than silently doing nothing.
|
|
958
|
+
this.lendingStatus = {
|
|
959
|
+
...this.lendingStatus,
|
|
960
|
+
user_has_browsed: false,
|
|
961
|
+
available_to_browse: true,
|
|
962
|
+
};
|
|
963
|
+
} else if (action === 'renew_loan') {
|
|
964
|
+
window?.IALendingIntervals?.clearAll();
|
|
950
965
|
this.loanRenewInProgress = false;
|
|
951
966
|
this.recoveringFromLoanExpiry = false;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
955
|
-
if (
|
|
956
|
-
errorMsg &&
|
|
957
|
-
action === 'renew_loan' &&
|
|
958
|
-
errorMsg.match(/not available/)
|
|
959
|
-
) {
|
|
960
|
-
this.showLoanUnavailableModal();
|
|
961
|
-
} else if (errorMsg && action !== 'create_token') {
|
|
962
|
-
this.showErrorModal(errorMsg, action);
|
|
963
|
-
}
|
|
964
967
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
968
|
+
// The loan was NOT actually renewed — reflect that immediately
|
|
969
|
+
// (show Borrow, and clear the stale timer value) instead of leaving
|
|
970
|
+
// the action bar showing "Return now" with a frozen countdown from
|
|
971
|
+
// before the failed attempt, which falsely implies the book is
|
|
972
|
+
// still actively being read.
|
|
973
|
+
this.lendingStatus = {
|
|
969
974
|
...this.lendingStatus,
|
|
970
975
|
user_has_browsed: false,
|
|
971
976
|
available_to_browse: true,
|
|
977
|
+
secondsLeftOnLoan: 0,
|
|
972
978
|
};
|
|
973
|
-
this.lendingStatus = currStatus;
|
|
974
|
-
}
|
|
975
979
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
980
|
+
// Show the real error and refresh the page on dismissal (the
|
|
981
|
+
// modal's Okay button) so the client picks up whatever the server
|
|
982
|
+
// now authoritatively considers true — matching on "not available"
|
|
983
|
+
// to guess the reason was fragile since the server can return any
|
|
984
|
+
// message (e.g. a lending limit).
|
|
985
|
+
this.showLoanUnavailableModal(errorMsg);
|
|
986
|
+
} else {
|
|
987
|
+
// Every other action failure genuinely affects loan state.
|
|
988
|
+
window?.IALendingIntervals?.clearAll();
|
|
989
|
+
|
|
990
|
+
if (!errorMsg) return;
|
|
991
|
+
|
|
992
|
+
this.showErrorModal(errorMsg, action);
|
|
993
|
+
|
|
994
|
+
// update action bar state if book is not available to browse or borrow.
|
|
995
|
+
if (errorMsg.match(/not available to borrow/gm)) {
|
|
996
|
+
if (action === 'browse_book') {
|
|
997
|
+
this.lendingStatus = { ...this.lendingStatus, available_to_browse: false };
|
|
998
|
+
} else if (action === 'borrow_book') {
|
|
999
|
+
this.lendingStatus = { ...this.lendingStatus, available_to_borrow: false };
|
|
1000
|
+
}
|
|
989
1001
|
}
|
|
990
|
-
this.lendingStatus = currStatus;
|
|
991
1002
|
}
|
|
992
1003
|
}
|
|
993
1004
|
|
|
@@ -900,6 +900,45 @@ describe('BookReader:userAction race regression (WEBDEV-8322)', () => {
|
|
|
900
900
|
expect(autoLoanRenewCheckerSpy.called).to.be.false;
|
|
901
901
|
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
902
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
|
+
});
|
|
903
942
|
});
|
|
904
943
|
|
|
905
944
|
describe('handleLendingActionError - loanRenewInProgress reset', () => {
|
|
@@ -978,6 +1017,110 @@ describe('handleLendingActionError - create_token failures must not stop the cou
|
|
|
978
1017
|
|
|
979
1018
|
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
980
1019
|
});
|
|
1020
|
+
|
|
1021
|
+
it('shows the same unavailable-modal-and-refresh path for any renew_loan failure, regardless of message text', async () => {
|
|
1022
|
+
// Regression: branching on errorMsg.match(/not available/) to decide
|
|
1023
|
+
// how to recover was fragile — the server can return any message
|
|
1024
|
+
// (e.g. a lending limit hit), and guessing which lendingStatus fields
|
|
1025
|
+
// are now accurate client-side is error-prone. Every renew_loan
|
|
1026
|
+
// failure now shows the real error and refreshes the page on
|
|
1027
|
+
// dismissal (showLoanUnavailableModal's Okay button), rather than
|
|
1028
|
+
// trying to patch up state without reloading from the server.
|
|
1029
|
+
const el = await fixture(
|
|
1030
|
+
container({
|
|
1031
|
+
userid: '@user1',
|
|
1032
|
+
identifier: 'foobar',
|
|
1033
|
+
lendingStatus: {
|
|
1034
|
+
is_lendable: true,
|
|
1035
|
+
user_has_browsed: true,
|
|
1036
|
+
browsingExpired: false,
|
|
1037
|
+
available_to_browse: false,
|
|
1038
|
+
secondsLeftOnLoan: 100,
|
|
1039
|
+
},
|
|
1040
|
+
})
|
|
1041
|
+
);
|
|
1042
|
+
await el.updateComplete;
|
|
1043
|
+
|
|
1044
|
+
const showUnavailableSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
1045
|
+
const lendingLimitMsg =
|
|
1046
|
+
'Your account has hit a lending limit. Please try again later or contact info@archive.org.';
|
|
1047
|
+
|
|
1048
|
+
el.handleLendingActionError({
|
|
1049
|
+
detail: {
|
|
1050
|
+
action: 'renew_loan',
|
|
1051
|
+
data: { error: lendingLimitMsg },
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
await el.updateComplete;
|
|
1055
|
+
|
|
1056
|
+
expect(showUnavailableSpy.calledOnceWith(lendingLimitMsg)).to.be.true;
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('updates lendingStatus immediately on a renew_loan failure — Borrow state, timer cleared', async () => {
|
|
1060
|
+
// The modal's Okay button refreshes the page, but that shouldn't be
|
|
1061
|
+
// the only thing correcting the UI — while the modal is still open,
|
|
1062
|
+
// the action bar must already show Borrow instead of "Return now"
|
|
1063
|
+
// with a stale countdown value from before the failed renewal.
|
|
1064
|
+
const el = await fixture(
|
|
1065
|
+
container({
|
|
1066
|
+
userid: '@user1',
|
|
1067
|
+
identifier: 'foobar',
|
|
1068
|
+
lendingStatus: {
|
|
1069
|
+
is_lendable: true,
|
|
1070
|
+
user_has_browsed: true,
|
|
1071
|
+
browsingExpired: false,
|
|
1072
|
+
available_to_browse: false,
|
|
1073
|
+
secondsLeftOnLoan: 100,
|
|
1074
|
+
},
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1077
|
+
await el.updateComplete;
|
|
1078
|
+
|
|
1079
|
+
el.handleLendingActionError({
|
|
1080
|
+
detail: {
|
|
1081
|
+
action: 'renew_loan',
|
|
1082
|
+
data: {
|
|
1083
|
+
error:
|
|
1084
|
+
'Your account has hit a lending limit. Please try again later or contact info@archive.org.',
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
await el.updateComplete;
|
|
1089
|
+
|
|
1090
|
+
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1091
|
+
expect(el.lendingStatus.available_to_browse).to.be.true;
|
|
1092
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1093
|
+
expect(el.primaryActions[0].text).to.not.equal('Return now');
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it('also shows the unavailable modal for the "not available" message, with its own text', async () => {
|
|
1097
|
+
const el = await fixture(
|
|
1098
|
+
container({
|
|
1099
|
+
userid: '@user1',
|
|
1100
|
+
identifier: 'foobar',
|
|
1101
|
+
lendingStatus: {
|
|
1102
|
+
is_lendable: true,
|
|
1103
|
+
user_has_browsed: true,
|
|
1104
|
+
browsingExpired: false,
|
|
1105
|
+
secondsLeftOnLoan: 100,
|
|
1106
|
+
},
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
await el.updateComplete;
|
|
1110
|
+
|
|
1111
|
+
const showUnavailableSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
1112
|
+
const notAvailableMsg = 'This book is not available to borrow at this time.';
|
|
1113
|
+
|
|
1114
|
+
el.handleLendingActionError({
|
|
1115
|
+
detail: {
|
|
1116
|
+
action: 'renew_loan',
|
|
1117
|
+
data: { error: notAvailableMsg },
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
await el.updateComplete;
|
|
1121
|
+
|
|
1122
|
+
expect(showUnavailableSpy.calledOnceWith(notAvailableMsg)).to.be.true;
|
|
1123
|
+
});
|
|
981
1124
|
});
|
|
982
1125
|
|
|
983
1126
|
describe('showWarningModal', () => {
|