@internetarchive/ia-book-actions 1.4.3 → 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 +416 -118
- package/package.json +2 -2
- package/src/assets/data/info.js +12 -0
- package/src/core/services/actions-handler/actions-handler-service.js +5 -1
- package/src/core/services/actions-handler/actions-handler.js +24 -18
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +2 -3
- package/src/ia-book-actions.js +243 -221
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/ia-book-actions.test.js +342 -7
|
@@ -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
|
+
});
|
|
@@ -82,7 +82,7 @@ describe('<ia-book-actions>', () => {
|
|
|
82
82
|
expect(el.disableActionGroup).to.be.true;
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it('handles `BookReader:userAction` event', async () => {
|
|
85
|
+
it('handles `BookReader:userAction` event when loan is active', async () => {
|
|
86
86
|
const el = await fixture(
|
|
87
87
|
container({
|
|
88
88
|
userid: '@user1',
|
|
@@ -91,8 +91,10 @@ describe('<ia-book-actions>', () => {
|
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
const spy = Sinon.spy(el, 'autoLoanRenewChecker');
|
|
94
|
-
el.
|
|
94
|
+
el.lendingStatus = { ...el.lendingStatus, browsingExpired: false };
|
|
95
95
|
await el.updateComplete;
|
|
96
|
+
// Set borrowType after the update so setupLendingToolbarActions() doesn't overwrite it
|
|
97
|
+
el.borrowType = 'browsed';
|
|
96
98
|
|
|
97
99
|
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
98
100
|
await el.updateComplete;
|
|
@@ -100,6 +102,25 @@ describe('<ia-book-actions>', () => {
|
|
|
100
102
|
expect(spy.calledOnce).to.be.true;
|
|
101
103
|
expect(spy.calledWith(true)).to.be.true;
|
|
102
104
|
});
|
|
105
|
+
|
|
106
|
+
it('handles `BookReader:userAction` event when loan has expired', async () => {
|
|
107
|
+
const el = await fixture(
|
|
108
|
+
container({
|
|
109
|
+
userid: '@user1',
|
|
110
|
+
identifier: 'foobar',
|
|
111
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: true },
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
116
|
+
el.borrowType = 'browsed';
|
|
117
|
+
await el.updateComplete;
|
|
118
|
+
|
|
119
|
+
window.dispatchEvent(new Event('BookReader:userAction'));
|
|
120
|
+
await el.updateComplete;
|
|
121
|
+
|
|
122
|
+
expect(spy.calledOnce).to.be.true;
|
|
123
|
+
});
|
|
103
124
|
});
|
|
104
125
|
|
|
105
126
|
describe('Primary Actions data', () => {
|
|
@@ -328,7 +349,9 @@ describe('Browsing expired status', () => {
|
|
|
328
349
|
await aTimeout(1500); // wait for 1.5 second
|
|
329
350
|
await el.updateComplete;
|
|
330
351
|
|
|
331
|
-
|
|
352
|
+
// Auto-return must not visibly change the action bar — it should stay
|
|
353
|
+
// exactly as it looked while reading (see WEBDEV-8322).
|
|
354
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
332
355
|
|
|
333
356
|
expect(el.timerCountdownEl).to.exist;
|
|
334
357
|
|
|
@@ -371,7 +394,9 @@ describe('Browsing expired status', () => {
|
|
|
371
394
|
await aTimeout(1500); // wait for 1.5 sec
|
|
372
395
|
|
|
373
396
|
expect(eventReceived).to.equal(true);
|
|
374
|
-
|
|
397
|
+
// Auto-return must not visibly change the action bar — it should stay
|
|
398
|
+
// exactly as it looked while reading (see WEBDEV-8322).
|
|
399
|
+
expect(el.primaryActions[0].text).to.equal('Return now');
|
|
375
400
|
expect(el.tokenPoller.loanTokenInterval).to.equal(undefined);
|
|
376
401
|
});
|
|
377
402
|
});
|
|
@@ -467,7 +492,7 @@ describe('Visibility change API for document', async () => {
|
|
|
467
492
|
});
|
|
468
493
|
});
|
|
469
494
|
|
|
470
|
-
it('when book is not expired', async () => {
|
|
495
|
+
it('when book is not expired and loan time is valid', async () => {
|
|
471
496
|
const el = await fixture(
|
|
472
497
|
container({
|
|
473
498
|
userid: '@user1',
|
|
@@ -480,31 +505,341 @@ describe('Visibility change API for document', async () => {
|
|
|
480
505
|
);
|
|
481
506
|
await el.updateComplete;
|
|
482
507
|
|
|
508
|
+
// Set a valid loan time well into the future
|
|
509
|
+
await el.localCache.set({
|
|
510
|
+
key: 'foobar-loanTime',
|
|
511
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
512
|
+
ttl: 3600,
|
|
513
|
+
});
|
|
514
|
+
|
|
483
515
|
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
484
516
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
517
|
+
await aTimeout(100);
|
|
518
|
+
|
|
519
|
+
expect(spy.calledOnce).to.be.true;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('when loan expired while tab was hidden (browsingExpired false, loanTime past)', async () => {
|
|
523
|
+
const el = await fixture(
|
|
524
|
+
container({
|
|
525
|
+
userid: '@user1',
|
|
526
|
+
identifier: 'foobar',
|
|
527
|
+
lendingStatus: {
|
|
528
|
+
user_has_browsed: true,
|
|
529
|
+
browsingExpired: false,
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
await el.updateComplete;
|
|
534
|
+
|
|
535
|
+
// Set a loan time already in the past
|
|
536
|
+
await el.localCache.set({
|
|
537
|
+
key: 'foobar-loanTime',
|
|
538
|
+
value: new Date(new Date().getTime() - 10 * 1000),
|
|
539
|
+
ttl: 3600,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
543
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
544
|
+
await aTimeout(100);
|
|
485
545
|
|
|
486
546
|
expect(spy.calledOnce).to.be.true;
|
|
487
547
|
});
|
|
488
548
|
|
|
489
|
-
it('when
|
|
549
|
+
it('when loan already marked expired (browsingExpired true)', async () => {
|
|
490
550
|
const el = await fixture(
|
|
491
551
|
container({
|
|
492
552
|
userid: '@user1',
|
|
493
553
|
identifier: 'foobar',
|
|
494
554
|
lendingStatus: {
|
|
495
555
|
user_has_browsed: true,
|
|
556
|
+
browsingExpired: true,
|
|
496
557
|
},
|
|
497
558
|
})
|
|
498
559
|
);
|
|
499
560
|
await el.updateComplete;
|
|
500
561
|
|
|
501
|
-
|
|
562
|
+
el.borrowType = 'browsed';
|
|
563
|
+
|
|
564
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
502
565
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
566
|
+
await aTimeout(100);
|
|
503
567
|
|
|
504
568
|
expect(spy.calledOnce).to.be.true;
|
|
505
569
|
});
|
|
506
570
|
});
|
|
507
571
|
|
|
572
|
+
describe('autoRenewExpiredLoan', () => {
|
|
573
|
+
it('optimistically resets browsingExpired so the button stays red', async () => {
|
|
574
|
+
const el = await fixture(
|
|
575
|
+
container({
|
|
576
|
+
userid: '@user1',
|
|
577
|
+
identifier: 'foobar',
|
|
578
|
+
lendingStatus: {
|
|
579
|
+
user_has_browsed: true,
|
|
580
|
+
available_to_browse: true,
|
|
581
|
+
browsingExpired: true,
|
|
582
|
+
},
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
await el.updateComplete;
|
|
586
|
+
|
|
587
|
+
// Before: browsingExpired true → borrow1HrAction → primary color is 'primary' (blue)
|
|
588
|
+
expect(el.primaryColor).to.equal('primary');
|
|
589
|
+
|
|
590
|
+
el.autoRenewExpiredLoan();
|
|
591
|
+
await el.updateComplete;
|
|
592
|
+
|
|
593
|
+
// After: browsingExpired flipped to false → patronIsReadingAction → 'danger' (red)
|
|
594
|
+
expect(el.lendingStatus.browsingExpired).to.be.false;
|
|
595
|
+
expect(el.primaryColor).to.equal('danger');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
|
|
599
|
+
const el = await fixture(
|
|
600
|
+
container({
|
|
601
|
+
userid: '@user1',
|
|
602
|
+
identifier: 'foobar',
|
|
603
|
+
lendingStatus: {
|
|
604
|
+
user_has_browsed: true,
|
|
605
|
+
browsingExpired: true,
|
|
606
|
+
},
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
await el.updateComplete;
|
|
610
|
+
|
|
611
|
+
el.autoRenewExpiredLoan();
|
|
612
|
+
|
|
613
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
614
|
+
|
|
615
|
+
// renewNow is set in a setTimeout — flush the macrotask queue
|
|
616
|
+
await aTimeout(50);
|
|
617
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
618
|
+
expect(el.loanRenewResult.renewType).to.equal('auto');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('ignores subsequent calls while a renewal is already in progress', async () => {
|
|
622
|
+
const el = await fixture(
|
|
623
|
+
container({
|
|
624
|
+
userid: '@user1',
|
|
625
|
+
identifier: 'foobar',
|
|
626
|
+
lendingStatus: {
|
|
627
|
+
user_has_browsed: true,
|
|
628
|
+
browsingExpired: true,
|
|
629
|
+
},
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
await el.updateComplete;
|
|
633
|
+
|
|
634
|
+
// Spy *after* fixture so the handler is the same instance
|
|
635
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
636
|
+
|
|
637
|
+
el.autoRenewExpiredLoan(); // first call — proceeds
|
|
638
|
+
el.autoRenewExpiredLoan(); // second call — should be a no-op
|
|
639
|
+
|
|
640
|
+
// The spy counts both invocations but the internal guard stops the second
|
|
641
|
+
expect(spy.callCount).to.equal(2);
|
|
642
|
+
// loanRenewInProgress is still true (not double-set or cleared)
|
|
643
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
644
|
+
|
|
645
|
+
// Only one setTimeout fires (the second call returned early)
|
|
646
|
+
await aTimeout(50);
|
|
647
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
|
|
651
|
+
const el = await fixture(
|
|
652
|
+
container({
|
|
653
|
+
userid: '@user1',
|
|
654
|
+
identifier: 'foobar',
|
|
655
|
+
lendingStatus: {
|
|
656
|
+
user_has_browsed: true,
|
|
657
|
+
browsingExpired: false,
|
|
658
|
+
secondsLeftOnLoan: 100,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
await el.updateComplete;
|
|
663
|
+
|
|
664
|
+
await el.localCache.set({
|
|
665
|
+
key: 'foobar-loanTime',
|
|
666
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
667
|
+
ttl: 3600,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
el.autoRenewExpiredLoan();
|
|
671
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
672
|
+
|
|
673
|
+
// Wait for the deferred renewNow to be set
|
|
674
|
+
await aTimeout(50);
|
|
675
|
+
|
|
676
|
+
// Simulate the renew_loan success response
|
|
677
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
678
|
+
'collapsible-action-group'
|
|
679
|
+
);
|
|
680
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
681
|
+
new CustomEvent('loanAutoRenewed', {
|
|
682
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
await aTimeout(100);
|
|
687
|
+
await el.updateComplete;
|
|
688
|
+
|
|
689
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
693
|
+
const el = await fixture(
|
|
694
|
+
container({
|
|
695
|
+
userid: '@user1',
|
|
696
|
+
identifier: 'foobar',
|
|
697
|
+
lendingStatus: {
|
|
698
|
+
user_has_browsed: true,
|
|
699
|
+
browsingExpired: true,
|
|
700
|
+
},
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
await el.updateComplete;
|
|
704
|
+
|
|
705
|
+
const showModalSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
706
|
+
|
|
707
|
+
el.autoRenewExpiredLoan();
|
|
708
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
709
|
+
|
|
710
|
+
el.handleLendingActionError({
|
|
711
|
+
detail: {
|
|
712
|
+
action: 'renew_loan',
|
|
713
|
+
data: { error: 'book is not available' },
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
718
|
+
expect(showModalSpy.calledOnce).to.be.true;
|
|
719
|
+
});
|
|
720
|
+
});
|
|
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
|
+
|
|
508
843
|
describe('Shared Resize Observer', () => {
|
|
509
844
|
it('can receive a Shared Resize Observer', async () => {
|
|
510
845
|
const sharedObserverStub = new SharedResizeObserver();
|