@internetarchive/ia-book-actions 1.4.4-alpha9 → 1.4.5
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 +38 -1
- package/package.json +1 -1
- package/src/assets/data/info-icon.js +17 -0
- package/src/core/services/actions-handler/actions-handler-service.js +27 -4
- package/src/core/services/actions-handler/actions-handler.js +77 -36
- package/src/core/services/loan-analytics.js +0 -11
- package/src/core/services/loan-renew-helper.js +2 -2
- package/src/core/services/loan-token-poller.js +87 -19
- package/src/ia-book-actions.js +290 -245
- package/test/core/services/loan-token-poller.test.js +111 -15
- package/test/ia-book-actions.test.js +503 -35
- package/src/assets/data/info.js +0 -12
|
@@ -121,6 +121,84 @@ describe('<ia-book-actions>', () => {
|
|
|
121
121
|
|
|
122
122
|
expect(spy.calledOnce).to.be.true;
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
it('ignores BookReader:userAction fired by BookReader init (WEBDEV-8322 follow-up)', async () => {
|
|
126
|
+
// Regression: BookReader.updateFirstIndex() fires 'userAction'
|
|
127
|
+
// unconditionally — including on BookReader's own init-time jump to
|
|
128
|
+
// the reader's last-read page (updateFromParams(), called from
|
|
129
|
+
// init()), which has nothing to do with the patron doing anything.
|
|
130
|
+
// Auto-renewing off that would silently re-borrow a lapsed book on a
|
|
131
|
+
// plain page refresh, before the patron did anything at all.
|
|
132
|
+
//
|
|
133
|
+
// BookReader.trigger() passes the BookReader instance as
|
|
134
|
+
// detail.props, and init() holds init.initComplete === false for
|
|
135
|
+
// exactly the span containing that jump.
|
|
136
|
+
const el = await fixture(
|
|
137
|
+
container({
|
|
138
|
+
userid: '@user1',
|
|
139
|
+
identifier: 'foobar',
|
|
140
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
el.borrowType = 'browsed';
|
|
144
|
+
await el.updateComplete;
|
|
145
|
+
|
|
146
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
147
|
+
|
|
148
|
+
window.dispatchEvent(
|
|
149
|
+
new CustomEvent('BookReader:userAction', {
|
|
150
|
+
detail: { props: { init: { initComplete: false } } },
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
await el.updateComplete;
|
|
154
|
+
|
|
155
|
+
expect(spy.called).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('honours BookReader:userAction once BookReader init has completed', async () => {
|
|
159
|
+
const el = await fixture(
|
|
160
|
+
container({
|
|
161
|
+
userid: '@user1',
|
|
162
|
+
identifier: 'foobar',
|
|
163
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
el.borrowType = 'browsed';
|
|
167
|
+
await el.updateComplete;
|
|
168
|
+
|
|
169
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
170
|
+
|
|
171
|
+
window.dispatchEvent(
|
|
172
|
+
new CustomEvent('BookReader:userAction', {
|
|
173
|
+
detail: { props: { init: { initComplete: true } } },
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
await el.updateComplete;
|
|
177
|
+
|
|
178
|
+
expect(spy.calledOnce).to.be.true;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('treats a userAction with no BookReader detail as a genuine interaction', async () => {
|
|
182
|
+
// A BookReader too old to expose init.initComplete, or any synthetic
|
|
183
|
+
// dispatch, must not be silently swallowed — only an explicit false
|
|
184
|
+
// suppresses the event.
|
|
185
|
+
const el = await fixture(
|
|
186
|
+
container({
|
|
187
|
+
userid: '@user1',
|
|
188
|
+
identifier: 'foobar',
|
|
189
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
el.borrowType = 'browsed';
|
|
193
|
+
await el.updateComplete;
|
|
194
|
+
|
|
195
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
196
|
+
|
|
197
|
+
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
198
|
+
await el.updateComplete;
|
|
199
|
+
|
|
200
|
+
expect(spy.calledOnce).to.be.true;
|
|
201
|
+
});
|
|
124
202
|
});
|
|
125
203
|
|
|
126
204
|
describe('Primary Actions data', () => {
|
|
@@ -401,8 +479,7 @@ describe('Browsing expired status', () => {
|
|
|
401
479
|
});
|
|
402
480
|
});
|
|
403
481
|
|
|
404
|
-
describe('Auto renew one hour loan',
|
|
405
|
-
await aTimeout(5000);
|
|
482
|
+
describe('Auto renew one hour loan', () => {
|
|
406
483
|
it('Book is browsing and renewed it now', async () => {
|
|
407
484
|
const loanTime = 88;
|
|
408
485
|
const el = await fixture(
|
|
@@ -475,8 +552,7 @@ describe('Auto renew one hour loan', async () => {
|
|
|
475
552
|
});
|
|
476
553
|
});
|
|
477
554
|
|
|
478
|
-
describe('Visibility change API for document',
|
|
479
|
-
await aTimeout(5000);
|
|
555
|
+
describe('Visibility change API for document', () => {
|
|
480
556
|
|
|
481
557
|
beforeEach(() => {
|
|
482
558
|
Object.defineProperty(document, 'hidden', {
|
|
@@ -500,6 +576,10 @@ describe('Visibility change API for document', async () => {
|
|
|
500
576
|
lendingStatus: {
|
|
501
577
|
user_has_browsed: true,
|
|
502
578
|
browsingExpired: false,
|
|
579
|
+
// Without this, startBrowseTimer() computes setTimeout(fn,
|
|
580
|
+
// undefined * 1000) -> NaN, which fires almost immediately and
|
|
581
|
+
// expires the loan before this test ever dispatches its event.
|
|
582
|
+
secondsLeftOnLoan: 3600,
|
|
503
583
|
},
|
|
504
584
|
})
|
|
505
585
|
);
|
|
@@ -527,6 +607,7 @@ describe('Visibility change API for document', async () => {
|
|
|
527
607
|
lendingStatus: {
|
|
528
608
|
user_has_browsed: true,
|
|
529
609
|
browsingExpired: false,
|
|
610
|
+
secondsLeftOnLoan: 3600,
|
|
530
611
|
},
|
|
531
612
|
})
|
|
532
613
|
);
|
|
@@ -595,6 +676,36 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
595
676
|
expect(el.primaryColor).to.equal('danger');
|
|
596
677
|
});
|
|
597
678
|
|
|
679
|
+
it('does not (re)start the countdown during the optimistic pre-confirmation window (WEBDEV-8322 follow-up)', async () => {
|
|
680
|
+
// Regression: the optimistic browsingExpired flip above triggers
|
|
681
|
+
// setupLendingToolbarActions() before renew_loan is even dispatched,
|
|
682
|
+
// let alone confirmed. Without gating on loanRenewInProgress, that
|
|
683
|
+
// would (re)start the timer-countdown interval using whatever stale
|
|
684
|
+
// secondsLeftOnLoan was left over from before the loan expired,
|
|
685
|
+
// showing a wrong/flickering value until handleLoanAutoRenewed()
|
|
686
|
+
// corrects it moments later.
|
|
687
|
+
const el = await fixture(
|
|
688
|
+
container({
|
|
689
|
+
userid: '@user1',
|
|
690
|
+
identifier: 'foobar',
|
|
691
|
+
lendingStatus: {
|
|
692
|
+
user_has_browsed: true,
|
|
693
|
+
available_to_browse: true,
|
|
694
|
+
browsingExpired: true,
|
|
695
|
+
},
|
|
696
|
+
})
|
|
697
|
+
);
|
|
698
|
+
await el.updateComplete;
|
|
699
|
+
|
|
700
|
+
const startTimerCountdownSpy = Sinon.spy(el, 'startTimerCountdown');
|
|
701
|
+
|
|
702
|
+
el.autoRenewExpiredLoan();
|
|
703
|
+
await el.updateComplete;
|
|
704
|
+
|
|
705
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
706
|
+
expect(startTimerCountdownSpy.called).to.be.false;
|
|
707
|
+
});
|
|
708
|
+
|
|
598
709
|
it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
|
|
599
710
|
const el = await fixture(
|
|
600
711
|
container({
|
|
@@ -660,9 +771,6 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
660
771
|
})
|
|
661
772
|
);
|
|
662
773
|
await el.updateComplete;
|
|
663
|
-
// Not testing the post-confirmation token-poller delay here — avoid
|
|
664
|
-
// leaving a real multi-second timer dangling past this test.
|
|
665
|
-
el.tokenPollerRenewalDelay = 0;
|
|
666
774
|
|
|
667
775
|
await el.localCache.set({
|
|
668
776
|
key: 'foobar-loanTime',
|
|
@@ -682,7 +790,7 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
682
790
|
);
|
|
683
791
|
collapsibleActionGroupEl.dispatchEvent(
|
|
684
792
|
new CustomEvent('loanAutoRenewed', {
|
|
685
|
-
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
793
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
686
794
|
})
|
|
687
795
|
);
|
|
688
796
|
|
|
@@ -712,9 +820,6 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
712
820
|
await el.updateComplete;
|
|
713
821
|
el.postInitComplete = true;
|
|
714
822
|
el.lendingBarPostInit = Sinon.spy();
|
|
715
|
-
// Use a small but non-zero delay to keep this test fast while still
|
|
716
|
-
// exercising the same "wait, then restart the poller" code path.
|
|
717
|
-
el.tokenPollerRenewalDelay = 50;
|
|
718
823
|
|
|
719
824
|
await el.localCache.set({
|
|
720
825
|
key: 'foobar-loanTime',
|
|
@@ -736,14 +841,11 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
736
841
|
);
|
|
737
842
|
collapsibleActionGroupEl.dispatchEvent(
|
|
738
843
|
new CustomEvent('loanAutoRenewed', {
|
|
739
|
-
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
844
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
740
845
|
})
|
|
741
846
|
);
|
|
742
847
|
|
|
743
|
-
|
|
744
|
-
// (datanode write-propagation buffer — see setupLendingToolbarActions)
|
|
745
|
-
// before the recovery case's first create_token call.
|
|
746
|
-
await aTimeout(250);
|
|
848
|
+
await aTimeout(200);
|
|
747
849
|
await el.updateComplete;
|
|
748
850
|
|
|
749
851
|
// The confirmed renewal must have forced a fresh create_token cycle
|
|
@@ -781,10 +883,6 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
781
883
|
ttl: 3600,
|
|
782
884
|
});
|
|
783
885
|
|
|
784
|
-
// Use a small but non-zero delay to keep this test fast while still
|
|
785
|
-
// exercising the same "wait, then restart the poller" code path.
|
|
786
|
-
el.tokenPollerRenewalDelay = 50;
|
|
787
|
-
|
|
788
886
|
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
789
887
|
|
|
790
888
|
el.autoRenewExpiredLoan();
|
|
@@ -799,13 +897,11 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
799
897
|
);
|
|
800
898
|
collapsibleActionGroupEl.dispatchEvent(
|
|
801
899
|
new CustomEvent('loanAutoRenewed', {
|
|
802
|
-
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
900
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
803
901
|
})
|
|
804
902
|
);
|
|
805
903
|
|
|
806
|
-
|
|
807
|
-
// write-propagation buffer — see setupLendingToolbarActions).
|
|
808
|
-
await aTimeout(250);
|
|
904
|
+
await aTimeout(200);
|
|
809
905
|
await el.updateComplete;
|
|
810
906
|
|
|
811
907
|
expect(startTokenPollerSpy.calledOnce).to.be.true;
|
|
@@ -837,7 +933,7 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
837
933
|
);
|
|
838
934
|
collapsibleActionGroupEl.dispatchEvent(
|
|
839
935
|
new CustomEvent('loanAutoRenewed', {
|
|
840
|
-
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
936
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
841
937
|
})
|
|
842
938
|
);
|
|
843
939
|
|
|
@@ -847,6 +943,105 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
847
943
|
expect(el.lendingBarPostInit.called).to.be.false;
|
|
848
944
|
});
|
|
849
945
|
|
|
946
|
+
it('also skips the initial create_token after a routine (non-expiry) renewal', async () => {
|
|
947
|
+
// BookLoanService::attempt_to_renew_loan() mints the access token as
|
|
948
|
+
// part of EVERY renew_loan response, not just a recovery from expiry.
|
|
949
|
+
// A routine top-up also has its poller stopped mid-renewal (see
|
|
950
|
+
// updated()'s clearAll() on loanRenewResult.renewNow), so it would
|
|
951
|
+
// otherwise fire this same redundant call once the poller restarts.
|
|
952
|
+
const el = await fixture(
|
|
953
|
+
container({
|
|
954
|
+
userid: '@user1',
|
|
955
|
+
identifier: 'foobar',
|
|
956
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: false },
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
await el.updateComplete;
|
|
960
|
+
el.postInitComplete = true;
|
|
961
|
+
el.reloadPageImages = Sinon.spy();
|
|
962
|
+
|
|
963
|
+
await el.localCache.set({
|
|
964
|
+
key: 'foobar-loanTime',
|
|
965
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
966
|
+
ttl: 3600,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
970
|
+
|
|
971
|
+
// Triggers updated()'s clearAll(), stopping the running poller, same
|
|
972
|
+
// as a real routine top-up would.
|
|
973
|
+
el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
974
|
+
await el.updateComplete;
|
|
975
|
+
|
|
976
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
977
|
+
'collapsible-action-group'
|
|
978
|
+
);
|
|
979
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
980
|
+
new CustomEvent('loanAutoRenewed', {
|
|
981
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
982
|
+
})
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
await aTimeout(200);
|
|
986
|
+
await el.updateComplete;
|
|
987
|
+
|
|
988
|
+
expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
|
|
989
|
+
// Retries any page image broken during the gap this renewal just closed.
|
|
990
|
+
expect(el.reloadPageImages.called).to.be.true;
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
|
|
994
|
+
// BookLoanService::attempt_to_renew_loan() now mints the access token
|
|
995
|
+
// as part of the renew_loan response itself (using the just-written
|
|
996
|
+
// loan record already in memory, not a separate read) -- so recovering
|
|
997
|
+
// from a genuine expiry no longer needs to wait on a confirming
|
|
998
|
+
// create_token success before re-initializing BookReader, and the
|
|
999
|
+
// poller it starts afterward shouldn't fire a redundant immediate call.
|
|
1000
|
+
const el = await fixture(
|
|
1001
|
+
container({
|
|
1002
|
+
userid: '@user1',
|
|
1003
|
+
identifier: 'foobar',
|
|
1004
|
+
lendingStatus: {
|
|
1005
|
+
user_has_browsed: true,
|
|
1006
|
+
browsingExpired: true,
|
|
1007
|
+
},
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
await el.updateComplete;
|
|
1011
|
+
el.lendingBarPostInit = Sinon.spy();
|
|
1012
|
+
el.reloadPageImages = Sinon.spy();
|
|
1013
|
+
|
|
1014
|
+
await el.localCache.set({
|
|
1015
|
+
key: 'foobar-loanTime',
|
|
1016
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
1017
|
+
ttl: 3600,
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1021
|
+
|
|
1022
|
+
el.autoRenewExpiredLoan();
|
|
1023
|
+
await el.updateComplete;
|
|
1024
|
+
await aTimeout(50); // let autoRenewExpiredLoan's setTimeout(0) set renewNow
|
|
1025
|
+
|
|
1026
|
+
await el.handleLoanAutoRenewed({
|
|
1027
|
+
detail: {
|
|
1028
|
+
action: 'renew_loan',
|
|
1029
|
+
data: { success: true, loan: { identifier: 'foobar', renewal: true } },
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// Called synchronously from handleLoanAutoRenewed, not via a
|
|
1034
|
+
// create_token success callback.
|
|
1035
|
+
expect(el.lendingBarPostInit.calledOnce).to.be.true;
|
|
1036
|
+
expect(el.postInitComplete).to.be.true;
|
|
1037
|
+
expect(el.reloadPageImages.called).to.be.true;
|
|
1038
|
+
|
|
1039
|
+
await el.updateComplete;
|
|
1040
|
+
await aTimeout(150); // let the 100ms token-poller-restart check run
|
|
1041
|
+
|
|
1042
|
+
expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
850
1045
|
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
851
1046
|
const el = await fixture(
|
|
852
1047
|
container({
|
|
@@ -980,8 +1175,75 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
|
|
|
980
1175
|
});
|
|
981
1176
|
});
|
|
982
1177
|
|
|
983
|
-
describe('handleLendingActionError - create_token
|
|
984
|
-
|
|
1178
|
+
describe('handleLendingActionError - every create_token failure resets the bar (WEBDEV-8322 follow-up)', () => {
|
|
1179
|
+
// A create_token failure can't be told apart from "the loan was actually
|
|
1180
|
+
// returned" (e.g. a stale loan-record cache on another node, or a genuine
|
|
1181
|
+
// revokeAccess() in BookReaderImages.php) from here, so initial and
|
|
1182
|
+
// routine interval failures are treated identically: no retry, reset to
|
|
1183
|
+
// Borrow/0, show the modal every time.
|
|
1184
|
+
for (const isInitial of [true, false]) {
|
|
1185
|
+
it(`shows the error modal and resets to Borrow/0 on a failed create_token (isInitial=${isInitial})`, async () => {
|
|
1186
|
+
const el = await fixture(
|
|
1187
|
+
container({
|
|
1188
|
+
userid: '@user1',
|
|
1189
|
+
identifier: 'foobar',
|
|
1190
|
+
lendingStatus: {
|
|
1191
|
+
user_has_browsed: true,
|
|
1192
|
+
browsingExpired: false,
|
|
1193
|
+
secondsLeftOnLoan: 100,
|
|
1194
|
+
},
|
|
1195
|
+
})
|
|
1196
|
+
);
|
|
1197
|
+
await el.updateComplete;
|
|
1198
|
+
|
|
1199
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
1200
|
+
const errorMsg = 'loan token not found. please try again later.';
|
|
1201
|
+
|
|
1202
|
+
el.handleLendingActionError({
|
|
1203
|
+
detail: {
|
|
1204
|
+
action: 'create_token',
|
|
1205
|
+
isInitial,
|
|
1206
|
+
data: { error: errorMsg },
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
|
|
1211
|
+
.true;
|
|
1212
|
+
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1213
|
+
expect(el.lendingStatus.available_to_browse).to.be.true;
|
|
1214
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it(`stops the countdown/expiry timers on a failed create_token (isInitial=${isInitial})`, async () => {
|
|
1218
|
+
const el = await fixture(
|
|
1219
|
+
container({
|
|
1220
|
+
userid: '@user1',
|
|
1221
|
+
identifier: 'foobar',
|
|
1222
|
+
lendingStatus: {
|
|
1223
|
+
user_has_browsed: true,
|
|
1224
|
+
browsingExpired: false,
|
|
1225
|
+
secondsLeftOnLoan: 100,
|
|
1226
|
+
},
|
|
1227
|
+
})
|
|
1228
|
+
);
|
|
1229
|
+
await el.updateComplete;
|
|
1230
|
+
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1231
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
|
|
1232
|
+
|
|
1233
|
+
el.handleLendingActionError({
|
|
1234
|
+
detail: {
|
|
1235
|
+
action: 'create_token',
|
|
1236
|
+
isInitial,
|
|
1237
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
1238
|
+
},
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
expect(window.IALendingIntervals.timerCountdown).to.equal(0);
|
|
1242
|
+
expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
it('shows the modal even without a specific error message', async () => {
|
|
985
1247
|
const el = await fixture(
|
|
986
1248
|
container({
|
|
987
1249
|
userid: '@user1',
|
|
@@ -994,18 +1256,14 @@ describe('handleLendingActionError - create_token failures must not stop the cou
|
|
|
994
1256
|
})
|
|
995
1257
|
);
|
|
996
1258
|
await el.updateComplete;
|
|
997
|
-
|
|
1259
|
+
|
|
1260
|
+
const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
|
|
998
1261
|
|
|
999
1262
|
el.handleLendingActionError({
|
|
1000
|
-
detail: {
|
|
1001
|
-
action: 'create_token',
|
|
1002
|
-
data: { error: 'loan token not found. please try again later.' },
|
|
1003
|
-
},
|
|
1263
|
+
detail: { action: 'create_token', isInitial: false, data: {} },
|
|
1004
1264
|
});
|
|
1005
1265
|
|
|
1006
|
-
|
|
1007
|
-
// nothing about how much time is left on the loan itself.
|
|
1008
|
-
expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
|
|
1266
|
+
expect(showErrorModalSpy.calledOnce).to.be.true;
|
|
1009
1267
|
});
|
|
1010
1268
|
|
|
1011
1269
|
it('still clears everything on a renew_loan failure', async () => {
|
|
@@ -1231,3 +1489,213 @@ describe('Shared Resize Observer', () => {
|
|
|
1231
1489
|
expect(component.sharedObserver).to.not.be.undefined;
|
|
1232
1490
|
});
|
|
1233
1491
|
});
|
|
1492
|
+
|
|
1493
|
+
describe('WEBDEV-8322 review fixes', () => {
|
|
1494
|
+
it('populates the action bar on a fresh load of an already-expired loan', async () => {
|
|
1495
|
+
// Regression: the hasExpired early-return was hoisted above the
|
|
1496
|
+
// GetLendingActions computation so auto-return wouldn't visibly change
|
|
1497
|
+
// the bar. That's right for the transition, but this method also runs
|
|
1498
|
+
// on the FIRST lendingStatus update, where there is no bar to preserve
|
|
1499
|
+
// — leaving the patron a blank bar and no way to re-borrow.
|
|
1500
|
+
const el = await fixture(
|
|
1501
|
+
container({
|
|
1502
|
+
userid: '@user1',
|
|
1503
|
+
identifier: 'fresh-expired',
|
|
1504
|
+
lendingStatus: {
|
|
1505
|
+
is_lendable: true,
|
|
1506
|
+
available_to_browse: true,
|
|
1507
|
+
available_to_borrow: true,
|
|
1508
|
+
user_has_browsed: true,
|
|
1509
|
+
browsingExpired: true,
|
|
1510
|
+
secondsLeftOnLoan: 0,
|
|
1511
|
+
},
|
|
1512
|
+
})
|
|
1513
|
+
);
|
|
1514
|
+
await el.updateComplete;
|
|
1515
|
+
|
|
1516
|
+
expect(el.primaryActions.length).to.be.greaterThan(0);
|
|
1517
|
+
expect(el.primaryActions.map(a => a.id)).to.include('browseBook');
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it('still leaves a rendered action bar untouched when the loan auto-returns', async () => {
|
|
1521
|
+
const el = await fixture(
|
|
1522
|
+
container({
|
|
1523
|
+
userid: '@user1',
|
|
1524
|
+
identifier: 'auto-returned',
|
|
1525
|
+
lendingStatus: {
|
|
1526
|
+
is_lendable: true,
|
|
1527
|
+
available_to_browse: true,
|
|
1528
|
+
available_to_borrow: true,
|
|
1529
|
+
user_has_browsed: true,
|
|
1530
|
+
browsingExpired: false,
|
|
1531
|
+
secondsLeftOnLoan: 300,
|
|
1532
|
+
},
|
|
1533
|
+
})
|
|
1534
|
+
);
|
|
1535
|
+
await el.updateComplete;
|
|
1536
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
1537
|
+
|
|
1538
|
+
el.lendingStatus = { ...el.lendingStatus, browsingExpired: true };
|
|
1539
|
+
await el.updateComplete;
|
|
1540
|
+
|
|
1541
|
+
// Auto-return must not visibly change the bar (the ticket's core ask).
|
|
1542
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it('does not treat a `renewal: false` response as a successful renewal', async () => {
|
|
1546
|
+
// Regression: handleLoanAutoRenewed saw a truthy `loan` and took the
|
|
1547
|
+
// success path. setBrowseTimeSession() never ran, so the loanTime read
|
|
1548
|
+
// was undefined -> NaN -> the fallback invented a full fresh hour,
|
|
1549
|
+
// putting a new countdown on screen next to the failure modal.
|
|
1550
|
+
const el = await fixture(
|
|
1551
|
+
container({
|
|
1552
|
+
userid: '@user1',
|
|
1553
|
+
identifier: 'not-renewed',
|
|
1554
|
+
lendingStatus: {
|
|
1555
|
+
is_lendable: true,
|
|
1556
|
+
available_to_browse: true,
|
|
1557
|
+
user_has_browsed: true,
|
|
1558
|
+
browsingExpired: true,
|
|
1559
|
+
secondsLeftOnLoan: 0,
|
|
1560
|
+
},
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
await el.updateComplete;
|
|
1564
|
+
el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
1565
|
+
el.loanRenewInProgress = true;
|
|
1566
|
+
await el.updateComplete;
|
|
1567
|
+
|
|
1568
|
+
await el.handleLoanAutoRenewed({
|
|
1569
|
+
detail: {
|
|
1570
|
+
action: 'renew_loan',
|
|
1571
|
+
data: { success: true, loan: { renewal: false } },
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
await el.updateComplete;
|
|
1575
|
+
|
|
1576
|
+
expect(el.lendingStatus.secondsLeftOnLoan).to.not.equal(
|
|
1577
|
+
el.loanRenewTimeConfig.loanTotalTime
|
|
1578
|
+
);
|
|
1579
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
it('clears loanRenewInProgress when the renewal response carries no loan', async () => {
|
|
1583
|
+
// Regression: the !activeLoan branch cleared recoveringFromLoanExpiry
|
|
1584
|
+
// but left loanRenewInProgress set, latching the component — every
|
|
1585
|
+
// later renewal attempt no-ops and neither the countdown nor the token
|
|
1586
|
+
// poller ever restarts.
|
|
1587
|
+
const el = await fixture(
|
|
1588
|
+
container({
|
|
1589
|
+
userid: '@user1',
|
|
1590
|
+
identifier: 'no-loan',
|
|
1591
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
1592
|
+
})
|
|
1593
|
+
);
|
|
1594
|
+
await el.updateComplete;
|
|
1595
|
+
|
|
1596
|
+
el.autoRenewExpiredLoan();
|
|
1597
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
1598
|
+
|
|
1599
|
+
await el.handleLoanAutoRenewed({
|
|
1600
|
+
detail: { action: 'renew_loan', data: { success: true } },
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
1604
|
+
expect(el.recoveringFromLoanExpiry).to.be.false;
|
|
1605
|
+
|
|
1606
|
+
// and the component is usable again, not latched
|
|
1607
|
+
el.autoRenewExpiredLoan();
|
|
1608
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
it('resets the bar and shows the modal on an interval create_token refresh failure too', async () => {
|
|
1612
|
+
// A routine refresh failing is treated the same as the initial one --
|
|
1613
|
+
// there's no reliable way to tell "harmless blip" from "the loan is
|
|
1614
|
+
// actually gone" from here (see handleLendingActionError).
|
|
1615
|
+
const el = await fixture(
|
|
1616
|
+
container({
|
|
1617
|
+
userid: '@user1',
|
|
1618
|
+
identifier: 'token-blip',
|
|
1619
|
+
lendingStatus: {
|
|
1620
|
+
is_lendable: true,
|
|
1621
|
+
available_to_browse: true,
|
|
1622
|
+
user_has_browsed: true,
|
|
1623
|
+
browsingExpired: false,
|
|
1624
|
+
secondsLeftOnLoan: 300,
|
|
1625
|
+
},
|
|
1626
|
+
})
|
|
1627
|
+
);
|
|
1628
|
+
await el.updateComplete;
|
|
1629
|
+
const spy = Sinon.spy(el, 'showErrorModal');
|
|
1630
|
+
|
|
1631
|
+
el.handleLendingActionError({
|
|
1632
|
+
detail: {
|
|
1633
|
+
action: 'create_token',
|
|
1634
|
+
isInitial: false,
|
|
1635
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
await el.updateComplete;
|
|
1639
|
+
|
|
1640
|
+
expect(spy.calledOnce).to.be.true;
|
|
1641
|
+
expect(el.lendingStatus.user_has_browsed).to.be.false;
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
it('tears down the previous poller before starting a new one', async () => {
|
|
1645
|
+
// Regression: startLoanTokenPoller() always replaced this.tokenPoller
|
|
1646
|
+
// without cancelling the old instance's pending internal retry timer,
|
|
1647
|
+
// which could later fire against stale state.
|
|
1648
|
+
const el = await fixture(
|
|
1649
|
+
container({
|
|
1650
|
+
userid: '@user1',
|
|
1651
|
+
identifier: 'poller-teardown',
|
|
1652
|
+
lendingStatus: {
|
|
1653
|
+
is_lendable: true,
|
|
1654
|
+
available_to_browse: true,
|
|
1655
|
+
user_has_browsed: true,
|
|
1656
|
+
browsingExpired: false,
|
|
1657
|
+
secondsLeftOnLoan: 300,
|
|
1658
|
+
},
|
|
1659
|
+
})
|
|
1660
|
+
);
|
|
1661
|
+
await el.updateComplete;
|
|
1662
|
+
|
|
1663
|
+
el.startLoanTokenPoller();
|
|
1664
|
+
const firstPoller = el.tokenPoller;
|
|
1665
|
+
const disconnectSpy = Sinon.spy(firstPoller, 'disconnectedCallback');
|
|
1666
|
+
|
|
1667
|
+
el.startLoanTokenPoller();
|
|
1668
|
+
|
|
1669
|
+
expect(disconnectSpy.calledOnce).to.be.true;
|
|
1670
|
+
expect(el.tokenPoller).to.not.equal(firstPoller);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
it('never restarts the poller on a create_token failure — no retry', async () => {
|
|
1674
|
+
const el = await fixture(
|
|
1675
|
+
container({
|
|
1676
|
+
userid: '@user1',
|
|
1677
|
+
identifier: 'token-exhausted',
|
|
1678
|
+
lendingStatus: {
|
|
1679
|
+
is_lendable: true,
|
|
1680
|
+
available_to_browse: true,
|
|
1681
|
+
user_has_browsed: true,
|
|
1682
|
+
browsingExpired: false,
|
|
1683
|
+
secondsLeftOnLoan: 300,
|
|
1684
|
+
},
|
|
1685
|
+
})
|
|
1686
|
+
);
|
|
1687
|
+
await el.updateComplete;
|
|
1688
|
+
|
|
1689
|
+
const startSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1690
|
+
|
|
1691
|
+
el.handleLendingActionError({
|
|
1692
|
+
detail: {
|
|
1693
|
+
action: 'create_token',
|
|
1694
|
+
isInitial: true,
|
|
1695
|
+
data: { error: 'loan token not found. please try again later.' },
|
|
1696
|
+
},
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
expect(startSpy.called).to.be.false;
|
|
1700
|
+
});
|
|
1701
|
+
});
|
package/src/assets/data/info.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { html } from 'lit';
|
|
2
|
-
|
|
3
|
-
export const infoIcon = html`<svg
|
|
4
|
-
height="20"
|
|
5
|
-
width="20"
|
|
6
|
-
viewBox="0 0 100 100"
|
|
7
|
-
xmlns="http://www.w3.org/2000/svg">
|
|
8
|
-
<g fill="none" fill-rule="evenodd"><path d="m0 0h100v100h-100z" fill="#fff"/>
|
|
9
|
-
<path d="m49.8315487 0h.1702245c6.7356878 0 13.1853038 1.31117332 19.3488483 3.93351997 6.1635444 2.62234664 11.4854233 6.15778963 15.9656369 10.60632903 4.4802135 4.4485394 8.0478347 9.7522946 10.7028636 15.9112655 2.655029 6.1589709 3.980878 12.6038012 3.9789971 19.3344909.0567419 6.6716279-1.1702933 13.0585776-3.6811042 19.1608491-2.510811 6.1022715-6.106803 11.5206067-10.7879759 16.2550055-9.7027949 9.7522946-21.4884754 14.6851412-35.3570414 14.79854h-.1702244c-6.7333236 0-13.1829397-1.3111733-19.3488483-3.93352-6.1659087-2.6223466-11.4877876-6.1577896-15.9656369-10.606329s-8.04547055-9.7522946-10.7028637-15.9112655c-2.65739314-6.1589709-3.9844243-12.6038012-3.98254337-19.3344909-.05674149-6.6716279 1.17029325-13.0585776 3.68110421-19.1608491 2.51081095-6.1022715 6.10680292-11.5206067 10.78797586-16.2550055 9.7027949-9.75229456 21.4884754-14.68514123 35.3570414-14.79854zm12.6566146 26.4757998c1.6745578-1.6828001 2.5118367-3.6747334 2.5118367-5.9757998 0-2.4126333-.8095238-4.4324583-2.4285714-6.059475s-3.6289796-2.440525-6.0297959-2.440525c-2.4008164 0-4.4107483.8135083-6.029796 2.440525-1.6745578 1.6270167-2.5118367 3.6468417-2.5118367 6.059475 0 2.1871753.8372789 4.1791086 2.5118367 5.9757998 1.6745579 1.6828001 3.6844898 2.5242002 6.029796 2.5242002 2.3453061 0 4.3274829-.8414001 5.9465306-2.5242002zm-12.1370589 52.7776981-1.2815282-.9486968c0-.4588935.398855-1.8938272 1.196565-4.3048011l12.7338588-39-23.0745876 3.6164609.2548896 3.873251c0-.1141289.4554971-.1997256 1.3664914-.2567901.9109942 0 1.623741.1723823 2.1382404.5171468.5121392.2306356.7965299.6039323.8531721 1.1198902 0 .8607225-.6549247 3.2134431-1.9647739 7.0581619l-8.1175252 24.1061729c-1.0242785 3.2716963-1.5080967 5.5388203-1.4514546 6.8013717.0566421 1.6643804.8826732 2.9839963 2.4780932 3.9588477 1.2532071.803658 2.8769482 1.205487 4.8712231 1.205487h.5982825c1.7653464-.0570645 3.8445846-.3875629 6.2377146-.9914952 2.3931299-.6039323 4.3590839-1.3933242 5.8978617-2.3681756 1.3098493-.803658 2.3919499-1.5359853 3.2463021-2.1969821.8543521-.6609968 1.3959925-1.1341564 1.6249211-1.4194788l.3433929-.3459533-2.8214861-3.3596708-2.9914125 2.0650205c-.79771.4588935-1.5104568.7454047-2.1382404.8595337z" fill="#000" fill-rule="nonzero"/>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|
|
12
|
-
`;
|