@internetarchive/ia-book-actions 1.4.4-alpha1 → 1.4.4-alpha2
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 +25 -6
- package/package.json +1 -1
- package/src/core/services/actions-handler/actions-handler-service.js +2 -2
- package/src/core/services/actions-handler/actions-handler.js +20 -15
- package/src/ia-book-actions.js +29 -8
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/ia-book-actions.test.js +121 -0
package/index.html
CHANGED
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
border: 2px solid #194880;
|
|
98
98
|
border-radius: 10px;
|
|
99
99
|
background: white;
|
|
100
|
+
height: 50px;
|
|
100
101
|
}
|
|
101
102
|
.timer-presets button.active-preset {
|
|
102
103
|
background: #194880;
|
|
@@ -137,6 +138,9 @@
|
|
|
137
138
|
.loan-status-badge.status-active {
|
|
138
139
|
background: #2e7d32; /* green: loan is currently checked out */
|
|
139
140
|
}
|
|
141
|
+
.loan-status-badge.status-renewing {
|
|
142
|
+
background: #b8860b; /* amber: a renew_loan attempt is in flight */
|
|
143
|
+
}
|
|
140
144
|
.loan-status-badge.status-returned {
|
|
141
145
|
background: #d9534f; /* red: auto-returned / not on loan */
|
|
142
146
|
}
|
|
@@ -216,7 +220,7 @@
|
|
|
216
220
|
console.log = (...consoleArgs) => {
|
|
217
221
|
__nativeConsoleLog(...consoleArgs);
|
|
218
222
|
|
|
219
|
-
const loggedPrefixes = ['[
|
|
223
|
+
const loggedPrefixes = ['[IABookActions]', '[IABookActions]', '[Demo]'];
|
|
220
224
|
if (typeof consoleArgs[0] !== 'string' || !loggedPrefixes.some((prefix) => consoleArgs[0].startsWith(prefix))) {
|
|
221
225
|
return;
|
|
222
226
|
}
|
|
@@ -280,7 +284,7 @@
|
|
|
280
284
|
const timerPresets = {
|
|
281
285
|
'15s': { loanTotalTime: 15, loanRenewAtLast: 13, pageChangedInLast: 5 },
|
|
282
286
|
'30s': { loanTotalTime: 30, loanRenewAtLast: 27, pageChangedInLast: 10 },
|
|
283
|
-
'2': { loanTotalTime: 120, loanRenewAtLast:
|
|
287
|
+
'2': { loanTotalTime: 120, loanRenewAtLast: 95, pageChangedInLast: 15 },
|
|
284
288
|
'5': { loanTotalTime: 300, loanRenewAtLast: 240, pageChangedInLast: 60 },
|
|
285
289
|
'10': { loanTotalTime: 600, loanRenewAtLast: 480, pageChangedInLast: 120 },
|
|
286
290
|
'30': { loanTotalTime: 1800, loanRenewAtLast: 1500, pageChangedInLast: 600 },
|
|
@@ -439,6 +443,14 @@
|
|
|
439
443
|
await iaBookActions.updateComplete;
|
|
440
444
|
}
|
|
441
445
|
|
|
446
|
+
// showWarningModal() reads its message text from
|
|
447
|
+
// loanRenewHelper.getMessageTexts(loanRenewResult.texts, ...) — that
|
|
448
|
+
// helper only exists, and loanRenewResult.texts only gets populated,
|
|
449
|
+
// once autoLoanRenewChecker() has run at least once (this is what the
|
|
450
|
+
// real periodic timer calls before ever showing the modal). Run it
|
|
451
|
+
// here too so the button shows the same populated message a real
|
|
452
|
+
// warning would.
|
|
453
|
+
await iaBookActions.autoLoanRenewChecker(false);
|
|
442
454
|
iaBookActions.showWarningModal();
|
|
443
455
|
});
|
|
444
456
|
|
|
@@ -578,6 +590,13 @@
|
|
|
578
590
|
function computeLoanStatus() {
|
|
579
591
|
const lendingStatus = iaBookActions.lendingStatus || {};
|
|
580
592
|
|
|
593
|
+
// Checked first: autoRenewExpiredLoan() optimistically flips
|
|
594
|
+
// browsingExpired back to false the moment a retry starts (before the
|
|
595
|
+
// renew_loan call has actually landed), so without this check the
|
|
596
|
+
// badge would misleadingly jump straight to ACTIVE during the retry.
|
|
597
|
+
if (iaBookActions.loanRenewInProgress) {
|
|
598
|
+
return { badge: 'RENEWING', className: 'status-renewing' };
|
|
599
|
+
}
|
|
581
600
|
if (lendingStatus.browsingExpired) {
|
|
582
601
|
return { badge: 'RETURNED', className: 'status-returned' };
|
|
583
602
|
}
|
|
@@ -662,7 +681,10 @@
|
|
|
662
681
|
<button id="show_expired_modal">2. Auto-return due to inactivity<br>(no modal, page stays put)</button>
|
|
663
682
|
</td>
|
|
664
683
|
<td class="action-button">
|
|
665
|
-
<button class="pageChangedEvent">
|
|
684
|
+
<button class="pageChangedEvent">
|
|
685
|
+
3. Interact with book (turn page)<br>
|
|
686
|
+
silently renew while active or after auto-return
|
|
687
|
+
</button>
|
|
666
688
|
</td>
|
|
667
689
|
<td class="action-button">
|
|
668
690
|
<button id="resync_timer">4. Return to tab after time away<br>(visibilitychange — silent retry if expired)</button>
|
|
@@ -688,9 +710,6 @@
|
|
|
688
710
|
<td class="searchParam">
|
|
689
711
|
<label><input type="checkbox" name="search" value="Search Param" />Search Param</label>
|
|
690
712
|
</td>
|
|
691
|
-
<td>
|
|
692
|
-
<label class="ia-button"><button class="pageChangedEvent">Page Change Event</button></label>
|
|
693
|
-
</td>
|
|
694
713
|
</tr>
|
|
695
714
|
</table>
|
|
696
715
|
</fieldset>
|
package/package.json
CHANGED
|
@@ -84,10 +84,10 @@ export default async function ActionsHandlerService(options) {
|
|
|
84
84
|
.then(data => {
|
|
85
85
|
// `data` is the parsed version of the JSON returned from the above endpoint.
|
|
86
86
|
if (!data?.error) {
|
|
87
|
-
log(`[
|
|
87
|
+
log(`[IABookActions] ✓ ${option.action} succeeded`, data);
|
|
88
88
|
option?.success(data);
|
|
89
89
|
} else {
|
|
90
|
-
log(`[
|
|
90
|
+
log(`[IABookActions] ✗ ${option.action} failed`, data);
|
|
91
91
|
option?.error(data);
|
|
92
92
|
}
|
|
93
93
|
});
|
|
@@ -53,20 +53,8 @@ export default class ActionsHandler extends LitElement {
|
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
this.addEventListener('autoRenew',
|
|
57
|
-
this.handleLoanRenewNow();
|
|
58
|
-
await this.loanAnanlytics?.storeLoanStatsCount(
|
|
59
|
-
this.identifier,
|
|
60
|
-
'autorenew'
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const analyticsLabel = detail?.renewType === 'auto' ? analyticsLabels.browseAutoRenew : analyticsLabels.browseManualRenew;
|
|
64
|
-
this.loanAnanlytics?.sendEvent(
|
|
65
|
-
analyticsCategories.browse,
|
|
66
|
-
analyticsActions.browseRenew,
|
|
67
|
-
analyticsLabel,
|
|
68
|
-
this.identifier
|
|
69
|
-
);
|
|
56
|
+
this.addEventListener('autoRenew', ({ detail }) => {
|
|
57
|
+
this.handleLoanRenewNow(detail?.renewType);
|
|
70
58
|
});
|
|
71
59
|
|
|
72
60
|
this.addEventListener('autoReturn', async () => {
|
|
@@ -177,7 +165,7 @@ export default class ActionsHandler extends LitElement {
|
|
|
177
165
|
});
|
|
178
166
|
}
|
|
179
167
|
|
|
180
|
-
handleLoanRenewNow() {
|
|
168
|
+
handleLoanRenewNow(renewType) {
|
|
181
169
|
const action = 'renew_loan';
|
|
182
170
|
|
|
183
171
|
ActionsHandlerService({
|
|
@@ -192,6 +180,23 @@ export default class ActionsHandler extends LitElement {
|
|
|
192
180
|
// Await — loanAutoRenewed listeners read this same cache key
|
|
193
181
|
// immediately; dispatching before the write lands was a race.
|
|
194
182
|
await this.setBrowseTimeSession();
|
|
183
|
+
|
|
184
|
+
// Only record the renew analytics event once the renewal has
|
|
185
|
+
// actually succeeded — not merely attempted.
|
|
186
|
+
await this.loanAnanlytics?.storeLoanStatsCount(
|
|
187
|
+
this.identifier,
|
|
188
|
+
'autorenew'
|
|
189
|
+
);
|
|
190
|
+
const analyticsLabel =
|
|
191
|
+
renewType === 'auto'
|
|
192
|
+
? analyticsLabels.browseAutoRenew
|
|
193
|
+
: analyticsLabels.browseManualRenew;
|
|
194
|
+
this.loanAnanlytics?.sendEvent(
|
|
195
|
+
analyticsCategories.browse,
|
|
196
|
+
analyticsActions.browseRenew,
|
|
197
|
+
analyticsLabel,
|
|
198
|
+
this.identifier
|
|
199
|
+
);
|
|
195
200
|
} else {
|
|
196
201
|
log('RENEW_LOAN ERROR --- ', {
|
|
197
202
|
action,
|
package/src/ia-book-actions.js
CHANGED
|
@@ -180,6 +180,13 @@ export default class IABookActions extends LitElement {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
if (changed.has('loanRenewResult') && this.loanRenewResult.renewNow) {
|
|
183
|
+
// Any renewal — not just the auto-return recovery path — pauses the
|
|
184
|
+
// countdown for the renew_loan round-trip. Flag it here, centrally,
|
|
185
|
+
// so every path (interaction during active reading, periodic
|
|
186
|
+
// checker, auto-return recovery) gets the same "renewing" signal
|
|
187
|
+
// instead of just looking frozen until handleLoanAutoRenewed() clears
|
|
188
|
+
// this and restarts the timer.
|
|
189
|
+
this.loanRenewInProgress = true;
|
|
183
190
|
window.IALendingIntervals.clearAll();
|
|
184
191
|
}
|
|
185
192
|
}
|
|
@@ -304,17 +311,27 @@ export default class IABookActions extends LitElement {
|
|
|
304
311
|
* dispatched this event from bookreader page changed
|
|
305
312
|
*/
|
|
306
313
|
window.addEventListener('BookReader:userAction', () => {
|
|
314
|
+
// Capture before autoRenewExpiredLoan() runs — it synchronously flips
|
|
315
|
+
// lendingStatus.browsingExpired to false as an optimistic UI update,
|
|
316
|
+
// so checking the live property afterward would always see it as
|
|
317
|
+
// false and wrongly let autoLoanRenewChecker() run too. That second
|
|
318
|
+
// call would then overwrite loanRenewResult.renewNow (set true by
|
|
319
|
+
// autoRenewExpiredLoan) back to false, since the loanTime cache entry
|
|
320
|
+
// was already deleted by browseHasExpired() — silently killing the
|
|
321
|
+
// in-flight renewal and leaving the countdown stuck.
|
|
322
|
+
const wasExpired = this.lendingStatus.browsingExpired;
|
|
323
|
+
|
|
307
324
|
log('[IABookActions] BookReader:userAction received', {
|
|
308
325
|
borrowType: this.borrowType,
|
|
309
|
-
browsingExpired:
|
|
326
|
+
browsingExpired: wasExpired,
|
|
310
327
|
});
|
|
311
328
|
|
|
312
329
|
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
313
|
-
if (
|
|
330
|
+
if (wasExpired) {
|
|
314
331
|
this.autoRenewExpiredLoan();
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
if (this.borrowType === 'browsed' && !
|
|
334
|
+
if (this.borrowType === 'browsed' && !wasExpired) {
|
|
318
335
|
this.autoLoanRenewChecker(true);
|
|
319
336
|
}
|
|
320
337
|
});
|
|
@@ -537,7 +554,7 @@ export default class IABookActions extends LitElement {
|
|
|
537
554
|
headline: '',
|
|
538
555
|
showCloseButton: false,
|
|
539
556
|
closeOnBackdropClick: false,
|
|
540
|
-
headerColor: '#
|
|
557
|
+
headerColor: '#d9534f',
|
|
541
558
|
message:
|
|
542
559
|
'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
|
|
543
560
|
});
|
|
@@ -545,7 +562,7 @@ export default class IABookActions extends LitElement {
|
|
|
545
562
|
const customModalContent = html`<br />
|
|
546
563
|
<div style="text-align: center">
|
|
547
564
|
<button
|
|
548
|
-
style="${modalButtonStyle.iaButton} ${modalButtonStyle.
|
|
565
|
+
style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
|
|
549
566
|
@click=${() => URLHelper.goToUrl(this.returnUrl, true)}
|
|
550
567
|
>
|
|
551
568
|
Okay
|
|
@@ -718,14 +735,15 @@ export default class IABookActions extends LitElement {
|
|
|
718
735
|
|
|
719
736
|
if (resyncd.hasSynced) {
|
|
720
737
|
secondsLeft = resyncd.whatShouldLeft;
|
|
721
|
-
log('[IABookActions]
|
|
738
|
+
log('[IABookActions] timer: timer re-synced', {
|
|
722
739
|
secondsLeft,
|
|
723
740
|
});
|
|
724
741
|
}
|
|
725
742
|
|
|
726
|
-
log('[IABookActions]
|
|
743
|
+
log('[IABookActions] timer', {
|
|
727
744
|
secondsLeft,
|
|
728
745
|
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
746
|
+
pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
|
|
729
747
|
});
|
|
730
748
|
|
|
731
749
|
/**
|
|
@@ -887,13 +905,16 @@ export default class IABookActions extends LitElement {
|
|
|
887
905
|
const action = event?.detail?.action;
|
|
888
906
|
const errorMsg = event?.detail?.data?.error;
|
|
889
907
|
|
|
908
|
+
if (action === 'renew_loan') {
|
|
909
|
+
this.loanRenewInProgress = false;
|
|
910
|
+
}
|
|
911
|
+
|
|
890
912
|
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
891
913
|
if (
|
|
892
914
|
errorMsg &&
|
|
893
915
|
action === 'renew_loan' &&
|
|
894
916
|
errorMsg.match(/not available/)
|
|
895
917
|
) {
|
|
896
|
-
this.loanRenewInProgress = false;
|
|
897
918
|
this.showLoanUnavailableModal();
|
|
898
919
|
} else if (errorMsg && action !== 'create_token') {
|
|
899
920
|
this.showErrorModal(errorMsg, action);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { expect, fixture } from '@open-wc/testing';
|
|
1
|
+
import { expect, fixture, aTimeout } from '@open-wc/testing';
|
|
2
2
|
import sinon from 'sinon';
|
|
3
|
+
import { LocalCache } from '@internetarchive/local-cache';
|
|
3
4
|
|
|
4
5
|
import ActionsHandler from '../../../src/core/services/actions-handler/actions-handler.js';
|
|
6
|
+
import { analyticsCategories, analyticsActions, analyticsLabels } from '../../../src/core/config/analytics-event-and-category.js';
|
|
5
7
|
|
|
6
8
|
// Define a temporary tag for this element for testing
|
|
7
9
|
const TEST_TAG = 'ia-actions-handler-test';
|
|
@@ -89,3 +91,110 @@ describe('ActionsHandler#setStickyAdminAccess', () => {
|
|
|
89
91
|
expect(lastSetCookie).to.contain(`${encodeURIComponent('sticky-admin-access')}=${encodeURIComponent('false')}`);
|
|
90
92
|
});
|
|
91
93
|
});
|
|
94
|
+
|
|
95
|
+
describe('ActionsHandler#handleLoanRenewNow (WEBDEV-8322)', () => {
|
|
96
|
+
let el;
|
|
97
|
+
let localCache;
|
|
98
|
+
const identifier = 'renew-now-test-book';
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
// Make sure no other test in this run left ?error=true in the URL —
|
|
102
|
+
// ActionsHandlerService reads it directly off window.location.
|
|
103
|
+
const params = new URLSearchParams(window.location.search);
|
|
104
|
+
if (params.has('error')) {
|
|
105
|
+
params.delete('error');
|
|
106
|
+
const query = params.toString();
|
|
107
|
+
window.history.replaceState(
|
|
108
|
+
{},
|
|
109
|
+
'',
|
|
110
|
+
`${window.location.pathname}${query ? `?${query}` : ''}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
el = await fixture(`<${TEST_TAG}></${TEST_TAG}>`);
|
|
115
|
+
el.identifier = identifier;
|
|
116
|
+
el.loanTotalTime = 3600;
|
|
117
|
+
localCache = new LocalCache({ namespace: 'loanRenew' });
|
|
118
|
+
el.localCache = localCache;
|
|
119
|
+
// The demo/test environment's ActionsHandlerService fakes a renew_loan
|
|
120
|
+
// success after a 5s delay to simulate a real network round-trip.
|
|
121
|
+
await localCache.delete(`${identifier}-loanTime`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
sinon.restore();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('awaits the loanTime cache write before dispatching loanAutoRenewed, and fires the renew analytics event only on success', async function test() {
|
|
129
|
+
this.timeout(7000);
|
|
130
|
+
|
|
131
|
+
const storeStatsSpy = sinon.spy(el.loanAnanlytics, 'storeLoanStatsCount');
|
|
132
|
+
const sendEventSpy = sinon.spy(el.loanAnanlytics, 'sendEvent');
|
|
133
|
+
|
|
134
|
+
let loanTimeWhenEventFired;
|
|
135
|
+
el.addEventListener('loanAutoRenewed', () => {
|
|
136
|
+
loanTimeWhenEventFired = localCache.get(`${identifier}-loanTime`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
el.handleLoanRenewNow('auto');
|
|
140
|
+
|
|
141
|
+
// ActionsHandlerService fakes a 5s delay for renew_loan in this
|
|
142
|
+
// (localhost) test environment — wait it out, matching the existing
|
|
143
|
+
// pattern used elsewhere in this test suite for the same fake delay.
|
|
144
|
+
await aTimeout(5500);
|
|
145
|
+
|
|
146
|
+
// The race this guards against: loanAutoRenewed used to dispatch
|
|
147
|
+
// before setBrowseTimeSession()'s cache write landed, so a listener
|
|
148
|
+
// reading the cache immediately could still see the old/deleted value.
|
|
149
|
+
const resolvedLoanTime = await loanTimeWhenEventFired;
|
|
150
|
+
expect(resolvedLoanTime).to.exist;
|
|
151
|
+
expect(resolvedLoanTime.getTime()).to.be.greaterThan(Date.now());
|
|
152
|
+
|
|
153
|
+
expect(storeStatsSpy.calledWith(identifier, 'autorenew')).to.be.true;
|
|
154
|
+
// storeLoanStatsCount fires its own matrix-stats sendEvent internally,
|
|
155
|
+
// so assert on the specific renew-success event rather than call count.
|
|
156
|
+
expect(
|
|
157
|
+
sendEventSpy.calledWith(
|
|
158
|
+
analyticsCategories.browse,
|
|
159
|
+
analyticsActions.browseRenew,
|
|
160
|
+
analyticsLabels.browseAutoRenew,
|
|
161
|
+
identifier
|
|
162
|
+
)
|
|
163
|
+
).to.be.true;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not fire the renew analytics event when the renewal fails', async () => {
|
|
167
|
+
const sendEventSpy = sinon.spy(el.loanAnanlytics, 'sendEvent');
|
|
168
|
+
const storeStatsSpy = sinon.spy(el.loanAnanlytics, 'storeLoanStatsCount');
|
|
169
|
+
|
|
170
|
+
// Force ActionsHandlerService's shouldReturnError branch for this call.
|
|
171
|
+
const originalSearch = window.location.search;
|
|
172
|
+
const params = new URLSearchParams(originalSearch);
|
|
173
|
+
params.set('error', 'true');
|
|
174
|
+
window.history.pushState({}, '', `?${params.toString()}`);
|
|
175
|
+
|
|
176
|
+
let errorEventDetail;
|
|
177
|
+
el.addEventListener('lendingActionError', ({ detail }) => {
|
|
178
|
+
errorEventDetail = detail;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
el.handleLoanRenewNow('auto');
|
|
183
|
+
await aTimeout(200);
|
|
184
|
+
|
|
185
|
+
expect(errorEventDetail?.action).to.equal('renew_loan');
|
|
186
|
+
// dispatchActionError fires its own 'LendingServiceError' analytics
|
|
187
|
+
// event on any error — that's expected. What must NOT fire is the
|
|
188
|
+
// renew-success event this fix gates on `isRenewal`.
|
|
189
|
+
expect(
|
|
190
|
+
sendEventSpy.calledWith(
|
|
191
|
+
analyticsCategories.browse,
|
|
192
|
+
analyticsActions.browseRenew
|
|
193
|
+
)
|
|
194
|
+
).to.be.false;
|
|
195
|
+
expect(storeStatsSpy.calledWith(identifier, 'autorenew')).to.be.false;
|
|
196
|
+
} finally {
|
|
197
|
+
window.history.pushState({}, '', `${window.location.pathname}${originalSearch}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -719,6 +719,127 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
719
719
|
});
|
|
720
720
|
});
|
|
721
721
|
|
|
722
|
+
describe('BookReader:userAction race regression (WEBDEV-8322)', () => {
|
|
723
|
+
it('does not let autoLoanRenewChecker clobber an in-flight expired-loan renewal', async () => {
|
|
724
|
+
// Get borrowType derived as 'browsed' first, same as the
|
|
725
|
+
// "Expiring book cancels interval" test above — hasExpired renders
|
|
726
|
+
// leave borrowType untouched, so it must already be 'browsed' before
|
|
727
|
+
// the loan is marked expired.
|
|
728
|
+
const baseStatus = {
|
|
729
|
+
is_lendable: true,
|
|
730
|
+
user_has_browsed: true,
|
|
731
|
+
browsingExpired: false,
|
|
732
|
+
};
|
|
733
|
+
const el = await fixture(
|
|
734
|
+
container({
|
|
735
|
+
userid: '@user1',
|
|
736
|
+
identifier: 'foobar',
|
|
737
|
+
lendingStatus: baseStatus,
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
await el.updateComplete;
|
|
741
|
+
expect(el.borrowType).to.equal('browsed');
|
|
742
|
+
|
|
743
|
+
el.lendingStatus = { ...baseStatus, browsingExpired: true };
|
|
744
|
+
await el.updateComplete;
|
|
745
|
+
|
|
746
|
+
const autoLoanRenewCheckerSpy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
747
|
+
|
|
748
|
+
window.dispatchEvent(new CustomEvent('BookReader:userAction'));
|
|
749
|
+
|
|
750
|
+
// autoRenewExpiredLoan()'s setTimeout(0) sets renewNow=true. If the old
|
|
751
|
+
// bug were present, the still-live lendingStatus.browsingExpired check
|
|
752
|
+
// would (wrongly) also let autoLoanRenewChecker(true) run — which reads
|
|
753
|
+
// the already-deleted loanTime cache key and overwrites renewNow back
|
|
754
|
+
// to false. Give both paths plenty of time to resolve.
|
|
755
|
+
await aTimeout(300);
|
|
756
|
+
|
|
757
|
+
expect(autoLoanRenewCheckerSpy.called).to.be.false;
|
|
758
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe('handleLendingActionError - loanRenewInProgress reset', () => {
|
|
763
|
+
it('clears loanRenewInProgress on any renew_loan failure, not just "not available"', async () => {
|
|
764
|
+
const el = await fixture(
|
|
765
|
+
container({
|
|
766
|
+
userid: '@user1',
|
|
767
|
+
identifier: 'foobar',
|
|
768
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
769
|
+
})
|
|
770
|
+
);
|
|
771
|
+
await el.updateComplete;
|
|
772
|
+
|
|
773
|
+
el.loanRenewInProgress = true;
|
|
774
|
+
|
|
775
|
+
el.handleLendingActionError({
|
|
776
|
+
detail: {
|
|
777
|
+
action: 'renew_loan',
|
|
778
|
+
data: { error: 'some other unrelated failure' },
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
describe('showWarningModal', () => {
|
|
787
|
+
it('shows a headline, one Okay button, and an info-icon help link; Okay dismisses without renewing', async () => {
|
|
788
|
+
const el = await fixture(
|
|
789
|
+
container({
|
|
790
|
+
userid: '@user1',
|
|
791
|
+
identifier: 'foobar',
|
|
792
|
+
lendingStatus: { user_has_browsed: true, secondsLeftOnLoan: 100 },
|
|
793
|
+
})
|
|
794
|
+
);
|
|
795
|
+
await el.updateComplete;
|
|
796
|
+
|
|
797
|
+
await el.showWarningModal();
|
|
798
|
+
|
|
799
|
+
const modalManagerEl = document.body.querySelector('modal-manager');
|
|
800
|
+
const modalTemplateEl =
|
|
801
|
+
modalManagerEl.shadowRoot.querySelector('modal-template');
|
|
802
|
+
const headline =
|
|
803
|
+
modalTemplateEl.shadowRoot.querySelector('.headline')?.textContent;
|
|
804
|
+
expect(headline).to.contain('Are you still there?');
|
|
805
|
+
|
|
806
|
+
const helpLink = modalTemplateEl.shadowRoot.querySelector(
|
|
807
|
+
'a[href="https://help.archive.org/help/borrowing-from-the-lending-library"]'
|
|
808
|
+
);
|
|
809
|
+
expect(helpLink).to.exist;
|
|
810
|
+
|
|
811
|
+
const buttons = modalManagerEl.shadowRoot.querySelectorAll(
|
|
812
|
+
'#book-action-bar-custom-buttons button'
|
|
813
|
+
);
|
|
814
|
+
expect(buttons.length).to.equal(1);
|
|
815
|
+
expect(buttons[0].textContent).to.contain('Okay');
|
|
816
|
+
|
|
817
|
+
buttons[0].click();
|
|
818
|
+
|
|
819
|
+
expect(el.warningModalOpen).to.be.false;
|
|
820
|
+
expect(el.warningModalDismissed).to.be.true;
|
|
821
|
+
expect(el.loanRenewResult.renewNow).to.be.false;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('does not reopen while already open', async () => {
|
|
825
|
+
const el = await fixture(
|
|
826
|
+
container({
|
|
827
|
+
userid: '@user1',
|
|
828
|
+
identifier: 'foobar',
|
|
829
|
+
lendingStatus: { user_has_browsed: true, secondsLeftOnLoan: 100 },
|
|
830
|
+
})
|
|
831
|
+
);
|
|
832
|
+
await el.updateComplete;
|
|
833
|
+
|
|
834
|
+
await el.showWarningModal();
|
|
835
|
+
const showModalSpy = Sinon.spy(el.modal, 'showModal');
|
|
836
|
+
|
|
837
|
+
await el.showWarningModal();
|
|
838
|
+
|
|
839
|
+
expect(showModalSpy.called).to.be.false;
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
722
843
|
describe('Shared Resize Observer', () => {
|
|
723
844
|
it('can receive a Shared Resize Observer', async () => {
|
|
724
845
|
const sharedObserverStub = new SharedResizeObserver();
|