@internetarchive/ia-book-actions 1.4.4-alpha1 → 1.4.4-alpha3
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 +39 -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
|
});
|
|
@@ -398,6 +415,16 @@ export default class IABookActions extends LitElement {
|
|
|
398
415
|
|
|
399
416
|
this.modal?.closeModal();
|
|
400
417
|
|
|
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
|
+
|
|
401
428
|
// Optimistically flip browsingExpired back to false so the action bar
|
|
402
429
|
// stays red (patronIsReadingAction) while the renew_loan request is in
|
|
403
430
|
// flight, instead of showing the blue "Borrow" state.
|
|
@@ -537,7 +564,7 @@ export default class IABookActions extends LitElement {
|
|
|
537
564
|
headline: '',
|
|
538
565
|
showCloseButton: false,
|
|
539
566
|
closeOnBackdropClick: false,
|
|
540
|
-
headerColor: '#
|
|
567
|
+
headerColor: '#d9534f',
|
|
541
568
|
message:
|
|
542
569
|
'Due to inactivity, this book was returned, and someone else has now borrowed it. Please try again later.',
|
|
543
570
|
});
|
|
@@ -545,7 +572,7 @@ export default class IABookActions extends LitElement {
|
|
|
545
572
|
const customModalContent = html`<br />
|
|
546
573
|
<div style="text-align: center">
|
|
547
574
|
<button
|
|
548
|
-
style="${modalButtonStyle.iaButton} ${modalButtonStyle.
|
|
575
|
+
style="${modalButtonStyle.iaButton} ${modalButtonStyle.return}"
|
|
549
576
|
@click=${() => URLHelper.goToUrl(this.returnUrl, true)}
|
|
550
577
|
>
|
|
551
578
|
Okay
|
|
@@ -718,14 +745,15 @@ export default class IABookActions extends LitElement {
|
|
|
718
745
|
|
|
719
746
|
if (resyncd.hasSynced) {
|
|
720
747
|
secondsLeft = resyncd.whatShouldLeft;
|
|
721
|
-
log('[IABookActions]
|
|
748
|
+
log('[IABookActions] timer: timer re-synced', {
|
|
722
749
|
secondsLeft,
|
|
723
750
|
});
|
|
724
751
|
}
|
|
725
752
|
|
|
726
|
-
log('[IABookActions]
|
|
753
|
+
log('[IABookActions] timer', {
|
|
727
754
|
secondsLeft,
|
|
728
755
|
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
756
|
+
pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
|
|
729
757
|
});
|
|
730
758
|
|
|
731
759
|
/**
|
|
@@ -887,13 +915,16 @@ export default class IABookActions extends LitElement {
|
|
|
887
915
|
const action = event?.detail?.action;
|
|
888
916
|
const errorMsg = event?.detail?.data?.error;
|
|
889
917
|
|
|
918
|
+
if (action === 'renew_loan') {
|
|
919
|
+
this.loanRenewInProgress = false;
|
|
920
|
+
}
|
|
921
|
+
|
|
890
922
|
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
891
923
|
if (
|
|
892
924
|
errorMsg &&
|
|
893
925
|
action === 'renew_loan' &&
|
|
894
926
|
errorMsg.match(/not available/)
|
|
895
927
|
) {
|
|
896
|
-
this.loanRenewInProgress = false;
|
|
897
928
|
this.showLoanUnavailableModal();
|
|
898
929
|
} else if (errorMsg && action !== 'create_token') {
|
|
899
930
|
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();
|