@internetarchive/ia-book-actions 1.4.4-alpha15 → 1.4.4-alpha17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/services/actions-handler/actions-handler-service.js +0 -3
- package/src/core/services/loan-token-poller.js +45 -4
- package/src/ia-book-actions.js +61 -9
- package/test/core/services/loan-token-poller.test.js +109 -24
- package/test/ia-book-actions.test.js +107 -4
package/package.json
CHANGED
|
@@ -117,8 +117,5 @@ export default async function ActionsHandlerService(options) {
|
|
|
117
117
|
* connection is exactly when this fires.
|
|
118
118
|
*/
|
|
119
119
|
log(`[IABookActions] ✗ ${option.action} threw`, error);
|
|
120
|
-
option?.error({
|
|
121
|
-
error: `Could not reach the lending service. Please check your connection and try again. (${error})`,
|
|
122
|
-
});
|
|
123
120
|
}
|
|
124
121
|
}
|
|
@@ -10,12 +10,46 @@ import log from './log.js';
|
|
|
10
10
|
* ActionsHandlerService is a function being used to execute
|
|
11
11
|
*/
|
|
12
12
|
export class LoanTokenPoller {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {string} options.identifier
|
|
16
|
+
* @param {string} options.borrowType
|
|
17
|
+
* @param {Function} options.successCallback - called after the initial
|
|
18
|
+
* loan token is created.
|
|
19
|
+
* @param {Function} options.errorCallback - called on any create_token
|
|
20
|
+
* failure (initial or a routine interval tick).
|
|
21
|
+
* @param {number} options.pollerDelay - interval between routine
|
|
22
|
+
* create_token checks, in seconds.
|
|
23
|
+
* @param {boolean} [options.skipInitialCall] - skip the immediate
|
|
24
|
+
* create_token call, only start the recurring interval. Used after a
|
|
25
|
+
* loan-expiry recovery renewal, where BookLoanService::
|
|
26
|
+
* attempt_to_renew_loan() already minted a valid access token as part
|
|
27
|
+
* of the renew_loan response itself — an immediate confirming
|
|
28
|
+
* create_token call is redundant.
|
|
29
|
+
* @param {Function} [options.onTokenRefreshed] - called after EVERY
|
|
30
|
+
* successful create_token, initial or routine — unlike successCallback,
|
|
31
|
+
* which only fires for the initial one. Lets the consumer retry any
|
|
32
|
+
* page image that failed during a prior access gap, now that access is
|
|
33
|
+
* confirmed good again.
|
|
34
|
+
*/
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
identifier,
|
|
38
|
+
borrowType,
|
|
39
|
+
successCallback,
|
|
40
|
+
errorCallback,
|
|
41
|
+
pollerDelay,
|
|
42
|
+
skipInitialCall = false,
|
|
43
|
+
onTokenRefreshed,
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
this.identifier = identifier;
|
|
15
47
|
this.borrowType = borrowType;
|
|
16
48
|
this.successCallback = successCallback; // callback function to be called after loan token is created
|
|
17
49
|
this.errorCallback = errorCallback; // callback function to be called after loan token is created
|
|
18
50
|
this.pollerDelay = pollerDelay; // value in seconds
|
|
51
|
+
this.skipInitialCall = skipInitialCall === true;
|
|
52
|
+
this.onTokenRefreshed = onTokenRefreshed;
|
|
19
53
|
|
|
20
54
|
this.loanTokenInterval = undefined;
|
|
21
55
|
|
|
@@ -34,8 +68,14 @@ export class LoanTokenPoller {
|
|
|
34
68
|
|
|
35
69
|
async bookAccessed() {
|
|
36
70
|
if (this.borrowType) {
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
if (this.skipInitialCall) {
|
|
72
|
+
log('[LoanTokenPoller] skipping initial create_token — already minted by the renewal response', {
|
|
73
|
+
identifier: this.identifier,
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
// Do an initial token, then set an interval
|
|
77
|
+
this.handleLoanTokenPoller(true);
|
|
78
|
+
}
|
|
39
79
|
|
|
40
80
|
// if this.borrowType = adminBorrowed,
|
|
41
81
|
// - we don't want to fetch token on interval
|
|
@@ -78,6 +118,7 @@ export class LoanTokenPoller {
|
|
|
78
118
|
identifier: this.identifier,
|
|
79
119
|
isInitial,
|
|
80
120
|
});
|
|
121
|
+
this.onTokenRefreshed?.();
|
|
81
122
|
if (isInitial) this.successCallback();
|
|
82
123
|
},
|
|
83
124
|
});
|
package/src/ia-book-actions.js
CHANGED
|
@@ -55,6 +55,10 @@ export default class IABookActions extends LitElement {
|
|
|
55
55
|
type: Function,
|
|
56
56
|
attribute: false,
|
|
57
57
|
},
|
|
58
|
+
reloadPageImages: {
|
|
59
|
+
type: Function,
|
|
60
|
+
attribute: false,
|
|
61
|
+
},
|
|
58
62
|
barType: { type: String },
|
|
59
63
|
sharedObserver: { attribute: false },
|
|
60
64
|
disableActionGroup: { type: Boolean },
|
|
@@ -77,6 +81,7 @@ export default class IABookActions extends LitElement {
|
|
|
77
81
|
this.width = 0;
|
|
78
82
|
this.bwbPurchaseUrl = '';
|
|
79
83
|
this.lendingBarPostInit = () => {};
|
|
84
|
+
this.reloadPageImages = () => {};
|
|
80
85
|
this.barType = 'action'; // 'title'|'action'
|
|
81
86
|
this.sharedObserver = undefined;
|
|
82
87
|
this.disableActionGroup = false;
|
|
@@ -104,6 +109,15 @@ export default class IABookActions extends LitElement {
|
|
|
104
109
|
*/
|
|
105
110
|
this.recoveringFromLoanExpiry = false;
|
|
106
111
|
|
|
112
|
+
/**
|
|
113
|
+
* True right after a confirmed renewal (any kind), until the token
|
|
114
|
+
* poller restarts. renew_loan's own response already carries a valid
|
|
115
|
+
* access token, so that restart should skip its normal immediate
|
|
116
|
+
* create_token call — see handleLoanAutoRenewed/
|
|
117
|
+
* setupLendingToolbarActions.
|
|
118
|
+
*/
|
|
119
|
+
this.skipNextInitialTokenCall = false;
|
|
120
|
+
|
|
107
121
|
this.warningModalOpen = false;
|
|
108
122
|
|
|
109
123
|
/** Once dismissed, don't re-show the warning modal every tick — only
|
|
@@ -318,8 +332,14 @@ export default class IABookActions extends LitElement {
|
|
|
318
332
|
!this.loanRenewInProgress &&
|
|
319
333
|
!window.IALendingIntervals.tokenPoller
|
|
320
334
|
) {
|
|
335
|
+
// Any confirmed renewal's response already minted a valid access
|
|
336
|
+
// token (see handleLoanAutoRenewed) — the poller only needs to
|
|
337
|
+
// start its recurring check, not fire an immediate confirming
|
|
338
|
+
// create_token call too.
|
|
339
|
+
const skipInitialCall = this.skipNextInitialTokenCall;
|
|
340
|
+
this.skipNextInitialTokenCall = false;
|
|
321
341
|
this.recoveringFromLoanExpiry = false;
|
|
322
|
-
this.startLoanTokenPoller();
|
|
342
|
+
this.startLoanTokenPoller(skipInitialCall);
|
|
323
343
|
}
|
|
324
344
|
}, 100);
|
|
325
345
|
}
|
|
@@ -740,10 +760,28 @@ export default class IABookActions extends LitElement {
|
|
|
740
760
|
ajaxResponse: detail?.data,
|
|
741
761
|
});
|
|
742
762
|
|
|
763
|
+
// BookLoanService::attempt_to_renew_loan() mints a valid access
|
|
764
|
+
// token as part of EVERY renew_loan response, not just a recovery
|
|
765
|
+
// from expiry — so the poller restart below never needs to fire a
|
|
766
|
+
// redundant confirming create_token call, regardless of which path
|
|
767
|
+
// triggered this renewal.
|
|
768
|
+
this.skipNextInitialTokenCall = true;
|
|
769
|
+
|
|
770
|
+
// Retry any page image that failed during the gap this renewal just
|
|
771
|
+
// closed, right away rather than waiting for the next routine
|
|
772
|
+
// create_token tick (up to tokenDelay seconds away, now that the
|
|
773
|
+
// initial one above is skipped).
|
|
774
|
+
this.reloadPageImages();
|
|
775
|
+
|
|
743
776
|
if (this.recoveringFromLoanExpiry) {
|
|
744
777
|
// Only now is it safe to re-initialize BookReader — not for a
|
|
745
|
-
// routine top-up, where that would be disruptive.
|
|
746
|
-
|
|
778
|
+
// routine top-up, where that would be disruptive. Call it
|
|
779
|
+
// directly rather than waiting on a confirming create_token
|
|
780
|
+
// success: BookLoanService::attempt_to_renew_loan() already
|
|
781
|
+
// minted a valid access token as part of this renew_loan
|
|
782
|
+
// response, so there's nothing left to confirm.
|
|
783
|
+
this.lendingBarPostInit();
|
|
784
|
+
this.postInitComplete = true;
|
|
747
785
|
}
|
|
748
786
|
|
|
749
787
|
const currStatus = {
|
|
@@ -895,9 +933,10 @@ export default class IABookActions extends LitElement {
|
|
|
895
933
|
|
|
896
934
|
/**
|
|
897
935
|
* enable access of borrowed/browsed books
|
|
936
|
+
* @param {boolean} [skipInitialCall] - see LoanTokenPoller's constructor doc
|
|
898
937
|
* @see LoanTokenPoller
|
|
899
938
|
*/
|
|
900
|
-
startLoanTokenPoller() {
|
|
939
|
+
startLoanTokenPoller(skipInitialCall = false) {
|
|
901
940
|
const successCallback = () => {
|
|
902
941
|
if (!this.postInitComplete) {
|
|
903
942
|
this.lendingBarPostInit();
|
|
@@ -907,17 +946,26 @@ export default class IABookActions extends LitElement {
|
|
|
907
946
|
const errorCallback = eventObj => {
|
|
908
947
|
this.handleLendingActionError(eventObj);
|
|
909
948
|
};
|
|
949
|
+
// BookReader has no self-healing for a page <img> that failed while
|
|
950
|
+
// access was briefly invalid — retry any of those now that this
|
|
951
|
+
// create_token confirms access is good again, on every success, not
|
|
952
|
+
// just the initial one.
|
|
953
|
+
const onTokenRefreshed = () => {
|
|
954
|
+
this.reloadPageImages();
|
|
955
|
+
};
|
|
910
956
|
|
|
911
957
|
// Tear down any previous poller first, so only one is ever controlling
|
|
912
958
|
// window.IALendingIntervals.tokenPoller at a time.
|
|
913
959
|
this.tokenPoller?.disconnectedCallback();
|
|
914
|
-
this.tokenPoller = new LoanTokenPoller(
|
|
915
|
-
this.identifier,
|
|
916
|
-
this.borrowType,
|
|
960
|
+
this.tokenPoller = new LoanTokenPoller({
|
|
961
|
+
identifier: this.identifier,
|
|
962
|
+
borrowType: this.borrowType,
|
|
917
963
|
successCallback,
|
|
918
964
|
errorCallback,
|
|
919
|
-
this.tokenDelay // in seconds
|
|
920
|
-
|
|
965
|
+
pollerDelay: this.tokenDelay, // in seconds
|
|
966
|
+
skipInitialCall,
|
|
967
|
+
onTokenRefreshed,
|
|
968
|
+
});
|
|
921
969
|
}
|
|
922
970
|
|
|
923
971
|
/*
|
|
@@ -1101,6 +1149,10 @@ export default class IABookActions extends LitElement {
|
|
|
1101
1149
|
justify-content: center;
|
|
1102
1150
|
flex-wrap: wrap;
|
|
1103
1151
|
}
|
|
1152
|
+
|
|
1153
|
+
#action-bar-modal {
|
|
1154
|
+
--modalWidth: 36rem;
|
|
1155
|
+
}
|
|
1104
1156
|
`;
|
|
1105
1157
|
}
|
|
1106
1158
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect } from '@open-wc/testing';
|
|
1
|
+
import { expect, aTimeout } from '@open-wc/testing';
|
|
2
2
|
import Sinon from 'sinon';
|
|
3
3
|
|
|
4
4
|
import { LoanTokenPoller } from '../../../src/core/services/loan-token-poller.js';
|
|
@@ -17,18 +17,17 @@ afterEach(() => {
|
|
|
17
17
|
|
|
18
18
|
describe('Get Loan Token', () => {
|
|
19
19
|
it('get loan token for browsed books', async () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
() => {
|
|
20
|
+
const tokenPoller = new LoanTokenPoller({
|
|
21
|
+
identifier: 'identifier1',
|
|
22
|
+
borrowType: 'browsed',
|
|
23
|
+
successCallback: () => {
|
|
25
24
|
console.log('success callback is executed!');
|
|
26
25
|
},
|
|
27
|
-
() => {
|
|
26
|
+
errorCallback: () => {
|
|
28
27
|
console.log('error callback is executed!');
|
|
29
28
|
},
|
|
30
|
-
2000 // 2 minutes'
|
|
31
|
-
);
|
|
29
|
+
pollerDelay: 2000, // 2 minutes'
|
|
30
|
+
});
|
|
32
31
|
const successCallbackSpy = Sinon.stub(tokenPoller, 'successCallback');
|
|
33
32
|
successCallbackSpy();
|
|
34
33
|
expect(successCallbackSpy.callCount).to.equal(1);
|
|
@@ -39,13 +38,13 @@ describe('Get Loan Token', () => {
|
|
|
39
38
|
});
|
|
40
39
|
|
|
41
40
|
it('get loan token for admin borrowed books', async () => {
|
|
42
|
-
const tokenPoller = new LoanTokenPoller(
|
|
43
|
-
'identifier1',
|
|
44
|
-
'adminBorrowed',
|
|
45
|
-
() => {},
|
|
46
|
-
() => {},
|
|
47
|
-
2000 // 2 minutes'
|
|
48
|
-
);
|
|
41
|
+
const tokenPoller = new LoanTokenPoller({
|
|
42
|
+
identifier: 'identifier1',
|
|
43
|
+
borrowType: 'adminBorrowed',
|
|
44
|
+
successCallback: () => {},
|
|
45
|
+
errorCallback: () => {},
|
|
46
|
+
pollerDelay: 2000, // 2 minutes'
|
|
47
|
+
});
|
|
49
48
|
|
|
50
49
|
const successCallbackSpy = Sinon.stub(tokenPoller, 'successCallback');
|
|
51
50
|
successCallbackSpy();
|
|
@@ -61,13 +60,13 @@ describe('handleTokenError (no retry)', () => {
|
|
|
61
60
|
// A create_token failure is treated as terminal on the first attempt —
|
|
62
61
|
// no retry, whether it's the "stale loan read" race or anything else.
|
|
63
62
|
const makeTokenPoller = () =>
|
|
64
|
-
new LoanTokenPoller(
|
|
65
|
-
'identifier1',
|
|
66
|
-
'browsed',
|
|
67
|
-
() => {},
|
|
68
|
-
() => {},
|
|
69
|
-
2000
|
|
70
|
-
);
|
|
63
|
+
new LoanTokenPoller({
|
|
64
|
+
identifier: 'identifier1',
|
|
65
|
+
borrowType: 'browsed',
|
|
66
|
+
successCallback: () => {},
|
|
67
|
+
errorCallback: () => {},
|
|
68
|
+
pollerDelay: 2000,
|
|
69
|
+
});
|
|
71
70
|
|
|
72
71
|
it('reports the error immediately, without retrying, on the initial call', () => {
|
|
73
72
|
const tokenPoller = makeTokenPoller();
|
|
@@ -100,7 +99,13 @@ describe('handleTokenError (no retry)', () => {
|
|
|
100
99
|
|
|
101
100
|
it('passes isInitial through to the error callback', () => {
|
|
102
101
|
const errorCallback = Sinon.spy();
|
|
103
|
-
const poller = new LoanTokenPoller(
|
|
102
|
+
const poller = new LoanTokenPoller({
|
|
103
|
+
identifier: 'foo',
|
|
104
|
+
borrowType: 'browsed',
|
|
105
|
+
successCallback: () => {},
|
|
106
|
+
errorCallback,
|
|
107
|
+
pollerDelay: 120,
|
|
108
|
+
});
|
|
104
109
|
poller.disconnectedCallback();
|
|
105
110
|
|
|
106
111
|
poller.handleTokenError({ error: 'something else went wrong' }, true);
|
|
@@ -109,3 +114,83 @@ describe('handleTokenError (no retry)', () => {
|
|
|
109
114
|
expect(errorCallback.firstCall.args[0].detail.isInitial).to.be.true;
|
|
110
115
|
});
|
|
111
116
|
});
|
|
117
|
+
|
|
118
|
+
describe('skipInitialCall option', () => {
|
|
119
|
+
it('starts the recurring interval but fires no immediate create_token call', () => {
|
|
120
|
+
const poller = new LoanTokenPoller({
|
|
121
|
+
identifier: 'foo',
|
|
122
|
+
borrowType: 'browsed',
|
|
123
|
+
successCallback: () => {},
|
|
124
|
+
errorCallback: () => {},
|
|
125
|
+
pollerDelay: 120,
|
|
126
|
+
skipInitialCall: true,
|
|
127
|
+
});
|
|
128
|
+
const handleLoanTokenPollerSpy = Sinon.spy(poller, 'handleLoanTokenPoller');
|
|
129
|
+
|
|
130
|
+
// bookAccessed() already ran in the constructor above (before the spy
|
|
131
|
+
// was attached) -- re-run it to observe whether it calls out.
|
|
132
|
+
poller.bookAccessed();
|
|
133
|
+
|
|
134
|
+
expect(handleLoanTokenPollerSpy.called).to.be.false;
|
|
135
|
+
expect(window.IALendingIntervals.tokenPoller).to.not.equal(0);
|
|
136
|
+
poller.disconnectedCallback();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('fires the immediate call as usual when skipInitialCall is not set', () => {
|
|
140
|
+
const poller = new LoanTokenPoller({
|
|
141
|
+
identifier: 'foo',
|
|
142
|
+
borrowType: 'browsed',
|
|
143
|
+
successCallback: () => {},
|
|
144
|
+
errorCallback: () => {},
|
|
145
|
+
pollerDelay: 120,
|
|
146
|
+
});
|
|
147
|
+
const handleLoanTokenPollerSpy = Sinon.spy(poller, 'handleLoanTokenPoller');
|
|
148
|
+
|
|
149
|
+
poller.bookAccessed();
|
|
150
|
+
|
|
151
|
+
expect(handleLoanTokenPollerSpy.calledWith(true)).to.be.true;
|
|
152
|
+
poller.disconnectedCallback();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('onTokenRefreshed option', () => {
|
|
157
|
+
// BookReader has no self-healing for a page <img> that failed while
|
|
158
|
+
// access was briefly invalid, so the consumer needs a signal on EVERY
|
|
159
|
+
// create_token success (not just the initial one) to know it's safe to
|
|
160
|
+
// retry those images.
|
|
161
|
+
it('fires on the initial create_token success', async () => {
|
|
162
|
+
const onTokenRefreshed = Sinon.spy();
|
|
163
|
+
const poller = new LoanTokenPoller({
|
|
164
|
+
identifier: 'foo',
|
|
165
|
+
borrowType: 'browsed',
|
|
166
|
+
successCallback: () => {},
|
|
167
|
+
errorCallback: () => {},
|
|
168
|
+
pollerDelay: 120,
|
|
169
|
+
onTokenRefreshed,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await aTimeout(50);
|
|
173
|
+
|
|
174
|
+
expect(onTokenRefreshed.called).to.be.true;
|
|
175
|
+
poller.disconnectedCallback();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('fires on a routine (non-initial) create_token success too', async () => {
|
|
179
|
+
const onTokenRefreshed = Sinon.spy();
|
|
180
|
+
const poller = new LoanTokenPoller({
|
|
181
|
+
identifier: 'foo',
|
|
182
|
+
borrowType: 'browsed',
|
|
183
|
+
successCallback: () => {},
|
|
184
|
+
errorCallback: () => {},
|
|
185
|
+
pollerDelay: 120,
|
|
186
|
+
onTokenRefreshed,
|
|
187
|
+
});
|
|
188
|
+
onTokenRefreshed.resetHistory();
|
|
189
|
+
|
|
190
|
+
await poller.handleLoanTokenPoller(false);
|
|
191
|
+
await aTimeout(50);
|
|
192
|
+
|
|
193
|
+
expect(onTokenRefreshed.called).to.be.true;
|
|
194
|
+
poller.disconnectedCallback();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -479,8 +479,7 @@ describe('Browsing expired status', () => {
|
|
|
479
479
|
});
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
-
describe('Auto renew one hour loan',
|
|
483
|
-
await aTimeout(5000);
|
|
482
|
+
describe('Auto renew one hour loan', () => {
|
|
484
483
|
it('Book is browsing and renewed it now', async () => {
|
|
485
484
|
const loanTime = 88;
|
|
486
485
|
const el = await fixture(
|
|
@@ -553,8 +552,7 @@ describe('Auto renew one hour loan', async () => {
|
|
|
553
552
|
});
|
|
554
553
|
});
|
|
555
554
|
|
|
556
|
-
describe('Visibility change API for document',
|
|
557
|
-
await aTimeout(5000);
|
|
555
|
+
describe('Visibility change API for document', () => {
|
|
558
556
|
|
|
559
557
|
beforeEach(() => {
|
|
560
558
|
Object.defineProperty(document, 'hidden', {
|
|
@@ -578,6 +576,10 @@ describe('Visibility change API for document', async () => {
|
|
|
578
576
|
lendingStatus: {
|
|
579
577
|
user_has_browsed: true,
|
|
580
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,
|
|
581
583
|
},
|
|
582
584
|
})
|
|
583
585
|
);
|
|
@@ -605,6 +607,7 @@ describe('Visibility change API for document', async () => {
|
|
|
605
607
|
lendingStatus: {
|
|
606
608
|
user_has_browsed: true,
|
|
607
609
|
browsingExpired: false,
|
|
610
|
+
secondsLeftOnLoan: 3600,
|
|
608
611
|
},
|
|
609
612
|
})
|
|
610
613
|
);
|
|
@@ -940,6 +943,106 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
940
943
|
expect(el.lendingBarPostInit.called).to.be.false;
|
|
941
944
|
});
|
|
942
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
|
+
// Retried right away rather than waiting for the (now-skipped) initial
|
|
990
|
+
// create_token or the next routine tick.
|
|
991
|
+
expect(el.reloadPageImages.called).to.be.true;
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
|
|
995
|
+
// BookLoanService::attempt_to_renew_loan() now mints the access token
|
|
996
|
+
// as part of the renew_loan response itself (using the just-written
|
|
997
|
+
// loan record already in memory, not a separate read) -- so recovering
|
|
998
|
+
// from a genuine expiry no longer needs to wait on a confirming
|
|
999
|
+
// create_token success before re-initializing BookReader, and the
|
|
1000
|
+
// poller it starts afterward shouldn't fire a redundant immediate call.
|
|
1001
|
+
const el = await fixture(
|
|
1002
|
+
container({
|
|
1003
|
+
userid: '@user1',
|
|
1004
|
+
identifier: 'foobar',
|
|
1005
|
+
lendingStatus: {
|
|
1006
|
+
user_has_browsed: true,
|
|
1007
|
+
browsingExpired: true,
|
|
1008
|
+
},
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
await el.updateComplete;
|
|
1012
|
+
el.lendingBarPostInit = Sinon.spy();
|
|
1013
|
+
el.reloadPageImages = Sinon.spy();
|
|
1014
|
+
|
|
1015
|
+
await el.localCache.set({
|
|
1016
|
+
key: 'foobar-loanTime',
|
|
1017
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
1018
|
+
ttl: 3600,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1022
|
+
|
|
1023
|
+
el.autoRenewExpiredLoan();
|
|
1024
|
+
await el.updateComplete;
|
|
1025
|
+
await aTimeout(50); // let autoRenewExpiredLoan's setTimeout(0) set renewNow
|
|
1026
|
+
|
|
1027
|
+
await el.handleLoanAutoRenewed({
|
|
1028
|
+
detail: {
|
|
1029
|
+
action: 'renew_loan',
|
|
1030
|
+
data: { success: true, loan: { identifier: 'foobar', renewal: true } },
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// Called synchronously from handleLoanAutoRenewed, not via a
|
|
1035
|
+
// create_token success callback.
|
|
1036
|
+
expect(el.lendingBarPostInit.calledOnce).to.be.true;
|
|
1037
|
+
expect(el.postInitComplete).to.be.true;
|
|
1038
|
+
expect(el.reloadPageImages.called).to.be.true;
|
|
1039
|
+
|
|
1040
|
+
await el.updateComplete;
|
|
1041
|
+
await aTimeout(150); // let the 100ms token-poller-restart check run
|
|
1042
|
+
|
|
1043
|
+
expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
|
|
1044
|
+
});
|
|
1045
|
+
|
|
943
1046
|
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
944
1047
|
const el = await fixture(
|
|
945
1048
|
container({
|