@internetarchive/ia-book-actions 1.4.3 → 1.4.4-alpha10
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 +25 -19
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +2 -3
- package/src/core/services/loan-token-poller.js +63 -16
- package/src/ia-book-actions.js +337 -247
- package/test/core/services/actions-handler.test.js +110 -1
- package/test/core/services/loan-renew-helper.test.js +1 -1
- package/test/core/services/loan-token-poller.test.js +76 -0
- package/test/ia-book-actions.test.js +716 -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
|
+
});
|
|
@@ -56,3 +56,79 @@ describe('Get Loan Token', () => {
|
|
|
56
56
|
expect(tokenPoller.loanTokenInterval).to.equal(undefined);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('handleTokenError retry (WEBDEV-8322 follow-up)', () => {
|
|
61
|
+
// Lending::do_renew() (backend) deletes the old loan record and writes
|
|
62
|
+
// a brand-new one on every renewal, so the initial create_token call
|
|
63
|
+
// right after a renewal can race that write's propagation and come
|
|
64
|
+
// back with this exact error even though the loan is genuinely valid.
|
|
65
|
+
const staleLoanReadError = {
|
|
66
|
+
error: 'You do not currently have this book borrowed.',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const makeTokenPoller = () =>
|
|
70
|
+
new LoanTokenPoller(
|
|
71
|
+
'identifier1',
|
|
72
|
+
'browsed',
|
|
73
|
+
() => {},
|
|
74
|
+
() => {},
|
|
75
|
+
2000
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it('retries the initial call on a stale-loan-read error instead of failing immediately', () => {
|
|
79
|
+
const tokenPoller = makeTokenPoller();
|
|
80
|
+
const handleLoanTokenPollerSpy = Sinon.spy(
|
|
81
|
+
tokenPoller,
|
|
82
|
+
'handleLoanTokenPoller'
|
|
83
|
+
);
|
|
84
|
+
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
85
|
+
const clock = Sinon.useFakeTimers();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
tokenPoller.handleTokenError(staleLoanReadError, true, 0);
|
|
89
|
+
|
|
90
|
+
expect(errorCallbackSpy.called).to.be.false;
|
|
91
|
+
|
|
92
|
+
clock.tick(tokenPoller.initialTokenRetryDelay);
|
|
93
|
+
|
|
94
|
+
expect(handleLoanTokenPollerSpy.calledWith(true, 1)).to.be.true;
|
|
95
|
+
} finally {
|
|
96
|
+
clock.restore();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('gives up and calls errorCallback after exhausting retries', () => {
|
|
101
|
+
const tokenPoller = makeTokenPoller();
|
|
102
|
+
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
103
|
+
|
|
104
|
+
tokenPoller.handleTokenError(
|
|
105
|
+
staleLoanReadError,
|
|
106
|
+
true,
|
|
107
|
+
tokenPoller.maxInitialTokenRetries
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(errorCallbackSpy.calledOnce).to.be.true;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not retry a routine (non-initial) poll on the same error', () => {
|
|
114
|
+
const tokenPoller = makeTokenPoller();
|
|
115
|
+
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
116
|
+
|
|
117
|
+
tokenPoller.handleTokenError(staleLoanReadError, false, 0);
|
|
118
|
+
|
|
119
|
+
expect(errorCallbackSpy.calledOnce).to.be.true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not retry a genuinely different error, even on the initial call', () => {
|
|
123
|
+
const tokenPoller = makeTokenPoller();
|
|
124
|
+
const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
|
|
125
|
+
|
|
126
|
+
tokenPoller.handleTokenError(
|
|
127
|
+
{ error: 'loan token not found. please try again later.' },
|
|
128
|
+
true,
|
|
129
|
+
0
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(errorCallbackSpy.calledOnce).to.be.true;
|
|
133
|
+
});
|
|
134
|
+
});
|