@internetarchive/ia-book-actions 1.4.4-alpha14 → 1.4.4-alpha16

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 CHANGED
@@ -360,7 +360,33 @@
360
360
  });
361
361
  });
362
362
 
363
-
363
+ // WEBDEV-8322 repro: fail only create_token (via ?failAction=create_token),
364
+ // leaving renew_loan (and every other action) succeeding normally —
365
+ // see actions-handler-service.js's shouldReturnError.
366
+ document.querySelectorAll('.failCreateToken input[type=checkbox]').forEach((item) => {
367
+ if (window.location.href.indexOf('?failAction=create_token') !== -1) {
368
+ item.checked = true;
369
+ }
370
+ item.addEventListener('click', (e) => {
371
+ console.log(`[Demo] fail-create_token-only mode ${e.target.checked ? 'enabled' : 'disabled'}`);
372
+
373
+ var searchParams = new URLSearchParams(window.location.search);
374
+ var separate = '';
375
+ if (e.target.checked) {
376
+ separate = '?';
377
+ searchParams.set('failAction', 'create_token');
378
+ } else {
379
+ separate = '';
380
+ searchParams.delete('failAction');
381
+ }
382
+
383
+ if (history.pushState) {
384
+ var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
385
+ window.history.pushState({path:newurl},'',newurl);
386
+ }
387
+ });
388
+ });
389
+
364
390
  document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
365
391
  item.addEventListener('click', (e) => {
366
392
  if (e.target.checked) {
@@ -726,6 +752,17 @@
726
752
  </span>
727
753
  </td>
728
754
  </tr>
755
+ <tr colspan='5'>
756
+ <td class="failCreateToken">
757
+ <label><input type="checkbox" name="failCreateToken" value="failCreateToken" />Fail create_token only (renew_loan still succeeds)</label>
758
+ <span style="color: #666; font-size: 11px; display: block; padding: 0 10px 10px;">
759
+ WEBDEV-8322 repro: unlike the checkbox above, this leaves renew_loan
760
+ succeeding and fails only the create_token call right after it — turn
761
+ this on, then run scenario 2 → 3 (or 2 → 4) below. Expect the error
762
+ modal, and the bar/timer resetting to Borrow/0 (no retry).
763
+ </span>
764
+ </td>
765
+ </tr>
729
766
  <tr>
730
767
  <td class="userState">
731
768
  <label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "author": "Internet Archive",
9
9
  "license": "AGPL-3.0-only",
10
- "version": "1.4.4-alpha14",
10
+ "version": "1.4.4-alpha16",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -18,7 +18,6 @@ export default async function ActionsHandlerService(options) {
18
18
  let baseHost = '/services/loans/loan';
19
19
  const location = window?.location;
20
20
 
21
- // return error reponse when not production and has ?error=true param...
22
21
  const tokenError = 'loan token not found. please try again later.';
23
22
  const borrowError =
24
23
  'This book is not available to borrow at this time. Please try again later.';
@@ -29,9 +28,18 @@ export default async function ActionsHandlerService(options) {
29
28
  'renew_loan',
30
29
  'return_loan',
31
30
  ];
31
+ const searchParams = new URLSearchParams(location?.search);
32
+ // ?error=true fails every erroneous action (e.g. simulates the book
33
+ // already being borrowed by someone else). ?failAction=create_token (or
34
+ // any action name) fails ONLY that action, so a renew_loan can succeed
35
+ // and the immediately-following create_token can be made to fail —
36
+ // reproducing WEBDEV-8322's "renewed, but create_token failed" bug
37
+ // without needing a real book.
32
38
  const shouldReturnError =
33
- new URLSearchParams(location?.search).get('error') === 'true' &&
34
- location?.hostname !== 'archive.org';
39
+ location?.hostname !== 'archive.org' &&
40
+ erroneousActions.includes(option?.action) &&
41
+ (searchParams.get('error') === 'true' ||
42
+ searchParams.get('failAction') === option?.action);
35
43
 
36
44
  const testHostname = ['localhost', 'internetarchive.github.io'];
37
45
  let isTest = false;
@@ -51,7 +59,7 @@ export default async function ActionsHandlerService(options) {
51
59
  })
52
60
  .then(async response => {
53
61
  // intentional error on localhost
54
- if (shouldReturnError && erroneousActions.includes(option?.action)) {
62
+ if (shouldReturnError) {
55
63
  return {
56
64
  success: false,
57
65
  error: option?.action === 'create_token' ? tokenError : borrowError,
@@ -109,8 +117,5 @@ export default async function ActionsHandlerService(options) {
109
117
  * connection is exactly when this fires.
110
118
  */
111
119
  log(`[IABookActions] ✗ ${option.action} threw`, error);
112
- option?.error({
113
- error: `Could not reach the lending service. Please check your connection and try again. (${error})`,
114
- });
115
120
  }
116
121
  }
@@ -10,35 +10,42 @@ import log from './log.js';
10
10
  * ActionsHandlerService is a function being used to execute
11
11
  */
12
12
  export class LoanTokenPoller {
13
- constructor(id, borrowType, successCallback, errorCallback, pollerDelay) {
14
- this.identifier = id;
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
+ */
30
+ constructor(options = {}) {
31
+ const {
32
+ identifier,
33
+ borrowType,
34
+ successCallback,
35
+ errorCallback,
36
+ pollerDelay,
37
+ skipInitialCall = false,
38
+ } = options;
39
+
40
+ this.identifier = identifier;
15
41
  this.borrowType = borrowType;
16
42
  this.successCallback = successCallback; // callback function to be called after loan token is created
17
43
  this.errorCallback = errorCallback; // callback function to be called after loan token is created
18
44
  this.pollerDelay = pollerDelay; // value in seconds
45
+ this.skipInitialCall = skipInitialCall === true;
19
46
 
20
47
  this.loanTokenInterval = undefined;
21
48
 
22
- /**
23
- * Lending::do_renew() (backend) deletes the old loan record and
24
- * writes a brand-new one on every renewal — this poller's initial
25
- * create_token call (fired right after a renewal re-establishes the
26
- * reading session) can race that write's propagation and come back
27
- * with "you do not currently have this book borrowed" even though
28
- * the loan is genuinely valid. Retry a few times with backoff instead
29
- * of giving up on the first attempt.
30
- */
31
- this.maxTokenRetries = 3;
32
- this.tokenRetryDelay = 1000; // ms, multiplied by attempt number
33
-
34
- /**
35
- * Handle for a pending retry, so teardown can cancel it. It's a bare
36
- * setTimeout rather than an IALendingIntervals entry, so without this
37
- * a retry scheduled just before the loan is returned would still fire
38
- * its create_token afterwards.
39
- */
40
- this.retryTimeout = undefined;
41
-
42
49
  /**
43
50
  * loan analytics instance
44
51
  * @see loan-analytics.js
@@ -50,14 +57,18 @@ export class LoanTokenPoller {
50
57
 
51
58
  disconnectedCallback() {
52
59
  window?.IALendingIntervals?.clearTokenPoller();
53
- clearTimeout(this.retryTimeout);
54
- this.retryTimeout = undefined;
55
60
  }
56
61
 
57
62
  async bookAccessed() {
58
63
  if (this.borrowType) {
59
- // Do an initial token, then set an interval
60
- this.handleLoanTokenPoller(true);
64
+ if (this.skipInitialCall) {
65
+ log('[LoanTokenPoller] skipping initial create_token — already minted by the renewal response', {
66
+ identifier: this.identifier,
67
+ });
68
+ } else {
69
+ // Do an initial token, then set an interval
70
+ this.handleLoanTokenPoller(true);
71
+ }
61
72
 
62
73
  // if this.borrowType = adminBorrowed,
63
74
  // - we don't want to fetch token on interval
@@ -84,24 +95,21 @@ export class LoanTokenPoller {
84
95
  /**
85
96
  * @param {boolean} isInitial - the first create_token call right after
86
97
  * bookAccessed()/a renewal, as opposed to a routine interval tick.
87
- * @param {number} retryCount - internal, counts retries of the initial call.
88
98
  */
89
- async handleLoanTokenPoller(isInitial = false, retryCount = 0) {
99
+ async handleLoanTokenPoller(isInitial = false) {
90
100
  const action = 'create_token';
91
101
  log('[LoanTokenPoller] create_token requested', {
92
102
  identifier: this.identifier,
93
103
  isInitial,
94
- retryCount,
95
104
  });
96
105
  ActionsHandlerService({
97
106
  identifier: this.identifier,
98
107
  action,
99
- error: data => this.handleTokenError(data, isInitial, retryCount),
108
+ error: data => this.handleTokenError(data, isInitial),
100
109
  success: () => {
101
110
  log('[LoanTokenPoller] create_token succeeded', {
102
111
  identifier: this.identifier,
103
112
  isInitial,
104
- retryCount,
105
113
  });
106
114
  if (isInitial) this.successCallback();
107
115
  },
@@ -109,58 +117,23 @@ export class LoanTokenPoller {
109
117
  }
110
118
 
111
119
  /**
112
- * Decide whether a create_token failure should be retried (a stale
113
- * read of the loan record right after a renewal, see the constructor
114
- * comment) or reported via errorCallback. Split out from
120
+ * Reports a create_token failure via errorCallback — no retry, it's
121
+ * treated as terminal on the first failure. Split out from
115
122
  * handleLoanTokenPoller so it's directly testable without needing to
116
123
  * mock the network call.
117
124
  *
118
125
  * @param {Object} data - the error payload from ActionsHandlerService.
119
126
  * @param {boolean} isInitial
120
- * @param {number} retryCount
121
127
  */
122
- handleTokenError(data, isInitial, retryCount) {
128
+ handleTokenError(data, isInitial) {
123
129
  const action = 'create_token';
124
- const isStaleLoanReadError =
125
- typeof data?.error === 'string' &&
126
- /do not currently have this book borrowed/i.test(data.error);
127
130
 
128
131
  log('[LoanTokenPoller] create_token failed', {
129
132
  identifier: this.identifier,
130
133
  isInitial,
131
- retryCount,
132
- isStaleLoanReadError,
133
134
  error: data?.error,
134
135
  });
135
136
 
136
- // The renewal write can be mid-propagation for an interval refresh too,
137
- // not just the initial call — a routine tick landing moments after a
138
- // renewal hits the same race. Retry both; isInitial only decides how a
139
- // final failure is reported, not whether it's worth retrying.
140
- if (isStaleLoanReadError && retryCount < this.maxTokenRetries) {
141
- const delay = this.tokenRetryDelay * (retryCount + 1);
142
- log(
143
- '[LoanTokenPoller] retrying create_token after stale-loan-read error',
144
- {
145
- identifier: this.identifier,
146
- nextRetryCount: retryCount + 1,
147
- delay,
148
- }
149
- );
150
- clearTimeout(this.retryTimeout);
151
- this.retryTimeout = setTimeout(() => {
152
- this.retryTimeout = undefined;
153
- this.handleLoanTokenPoller(isInitial, retryCount + 1);
154
- }, delay);
155
- return;
156
- }
157
-
158
- log('[LoanTokenPoller] giving up on create_token, reporting error', {
159
- identifier: this.identifier,
160
- isInitial,
161
- retryCount,
162
- });
163
-
164
137
  // isInitial rides along so the consumer can distinguish "the book won't
165
138
  // open at all" from "a mid-read refresh blipped" — see
166
139
  // IABookActions.handleLendingActionError.
@@ -96,18 +96,6 @@ export default class IABookActions extends LitElement {
96
96
 
97
97
  this.loanRenewInProgress = false;
98
98
 
99
- /**
100
- * True while a create_token error modal is up and the token poller is
101
- * retrying in the background. Cleared by startLoanTokenPoller()'s
102
- * successCallback. See handleLendingActionError.
103
- */
104
- this.awaitingTokenRecovery = false;
105
-
106
- /** Bounds how many times handleLendingActionError restarts the poller
107
- * for one outage, so a genuinely-lost loan can't retry forever. */
108
- this.tokenRecoveryAttempts = 0;
109
- this.maxTokenRecoveryAttempts = 3;
110
-
111
99
  /**
112
100
  * True while recovering from a loan that genuinely lapsed (set by
113
101
  * autoRenewExpiredLoan), as opposed to a routine pre-expiry top-up.
@@ -116,6 +104,15 @@ export default class IABookActions extends LitElement {
116
104
  */
117
105
  this.recoveringFromLoanExpiry = false;
118
106
 
107
+ /**
108
+ * True right after a confirmed renewal (any kind), until the token
109
+ * poller restarts. renew_loan's own response already carries a valid
110
+ * access token, so that restart should skip its normal immediate
111
+ * create_token call — see handleLoanAutoRenewed/
112
+ * setupLendingToolbarActions.
113
+ */
114
+ this.skipNextInitialTokenCall = false;
115
+
119
116
  this.warningModalOpen = false;
120
117
 
121
118
  /** Once dismissed, don't re-show the warning modal every tick — only
@@ -301,14 +298,17 @@ export default class IABookActions extends LitElement {
301
298
 
302
299
  if (!this.applyLendingActions()) return;
303
300
 
304
- // Don't (re)start the countdown mid-renewal (stale secondsLeftOnLoan)
305
- // or while recovering a create_token failure (timers stopped
306
- // deliberately — see handleLendingActionError).
307
- if (
308
- this.borrowType === 'browsed' &&
309
- !this.loanRenewInProgress &&
310
- !this.awaitingTokenRecovery
311
- ) {
301
+ // primaryActions/borrowType/etc are plain fields, not reactive
302
+ // properties, so nothing pushes them to the rendered template (and
303
+ // from there to <collapsible-action-group>) on its own. Do that now,
304
+ // unconditionally — the early returns below (not borrowed, title bar)
305
+ // must not skip it, or the action bar visibly goes stale (e.g. a
306
+ // create_token failure resetting to Borrow never actually shows it).
307
+ this.requestUpdate();
308
+
309
+ // Don't (re)start the countdown mid-renewal — secondsLeftOnLoan could
310
+ // still be stale until handleLoanAutoRenewed() confirms it.
311
+ if (this.borrowType === 'browsed' && !this.loanRenewInProgress) {
312
312
  await this.startTimerCountdown();
313
313
  await this.startBrowseTimer();
314
314
  }
@@ -327,12 +327,16 @@ export default class IABookActions extends LitElement {
327
327
  !this.loanRenewInProgress &&
328
328
  !window.IALendingIntervals.tokenPoller
329
329
  ) {
330
+ // Any confirmed renewal's response already minted a valid access
331
+ // token (see handleLoanAutoRenewed) — the poller only needs to
332
+ // start its recurring check, not fire an immediate confirming
333
+ // create_token call too.
334
+ const skipInitialCall = this.skipNextInitialTokenCall;
335
+ this.skipNextInitialTokenCall = false;
330
336
  this.recoveringFromLoanExpiry = false;
331
- this.startLoanTokenPoller();
337
+ this.startLoanTokenPoller(skipInitialCall);
332
338
  }
333
339
  }, 100);
334
-
335
- this.requestUpdate();
336
340
  }
337
341
 
338
342
  /**
@@ -405,10 +409,6 @@ export default class IABookActions extends LitElement {
405
409
 
406
410
  if (this.borrowType !== 'browsed') return;
407
411
 
408
- // Timers are deliberately stopped during create_token recovery
409
- // (see handleLendingActionError) — leave them alone here.
410
- if (this.awaitingTokenRecovery) return;
411
-
412
412
  if (this.lendingStatus.browsingExpired === false) {
413
413
  const loanTime = await this.localCache.get(
414
414
  `${this.identifier}-loanTime`
@@ -442,11 +442,9 @@ export default class IABookActions extends LitElement {
442
442
  * @param {Boolean} hasPageChanged
443
443
  */
444
444
  async autoLoanRenewChecker(hasPageChanged = false) {
445
- // Re-entrancy guard against rapid BookReader:userAction events, and
446
- // against restarting a renewal while still recovering the last one's
447
- // create_token (loanRenewInProgress clears as soon as renew_loan
448
- // itself succeeds, before that's known).
449
- if (this.loanRenewInProgress || this.awaitingTokenRecovery) return;
445
+ // Re-entrancy guard against rapid BookReader:userAction events (e.g. a
446
+ // single scroll firing several in quick succession).
447
+ if (this.loanRenewInProgress) return;
450
448
 
451
449
  this.loanRenewHelper = new LoanRenewHelper(
452
450
  hasPageChanged,
@@ -465,14 +463,10 @@ export default class IABookActions extends LitElement {
465
463
  * handleLendingActionError() handle the outcome either way.
466
464
  */
467
465
  autoRenewExpiredLoan() {
468
- if (this.loanRenewInProgress || this.awaitingTokenRecovery) {
466
+ if (this.loanRenewInProgress) {
469
467
  log(
470
468
  '[IABookActions] autoRenewExpiredLoan: skipped, renewal already in progress',
471
- {
472
- identifier: this.identifier,
473
- loanRenewInProgress: this.loanRenewInProgress,
474
- awaitingTokenRecovery: this.awaitingTokenRecovery,
475
- }
469
+ { identifier: this.identifier }
476
470
  );
477
471
  return;
478
472
  }
@@ -761,10 +755,22 @@ export default class IABookActions extends LitElement {
761
755
  ajaxResponse: detail?.data,
762
756
  });
763
757
 
758
+ // BookLoanService::attempt_to_renew_loan() mints a valid access
759
+ // token as part of EVERY renew_loan response, not just a recovery
760
+ // from expiry — so the poller restart below never needs to fire a
761
+ // redundant confirming create_token call, regardless of which path
762
+ // triggered this renewal.
763
+ this.skipNextInitialTokenCall = true;
764
+
764
765
  if (this.recoveringFromLoanExpiry) {
765
766
  // Only now is it safe to re-initialize BookReader — not for a
766
- // routine top-up, where that would be disruptive.
767
- this.postInitComplete = false;
767
+ // routine top-up, where that would be disruptive. Call it
768
+ // directly rather than waiting on a confirming create_token
769
+ // success: BookLoanService::attempt_to_renew_loan() already
770
+ // minted a valid access token as part of this renew_loan
771
+ // response, so there's nothing left to confirm.
772
+ this.lendingBarPostInit();
773
+ this.postInitComplete = true;
768
774
  }
769
775
 
770
776
  const currStatus = {
@@ -916,59 +922,31 @@ export default class IABookActions extends LitElement {
916
922
 
917
923
  /**
918
924
  * enable access of borrowed/browsed books
925
+ * @param {boolean} [skipInitialCall] - see LoanTokenPoller's constructor doc
919
926
  * @see LoanTokenPoller
920
927
  */
921
- startLoanTokenPoller() {
922
- const successCallback = async () => {
928
+ startLoanTokenPoller(skipInitialCall = false) {
929
+ const successCallback = () => {
923
930
  if (!this.postInitComplete) {
924
931
  this.lendingBarPostInit();
925
932
  }
926
933
  this.postInitComplete = true;
927
-
928
- if (this.awaitingTokenRecovery) {
929
- log(
930
- '[IABookActions] create_token recovered — closing error modal, resuming timer',
931
- { identifier: this.identifier }
932
- );
933
- this.awaitingTokenRecovery = false;
934
- this.tokenRecoveryAttempts = 0;
935
- this.modal?.closeModal();
936
- this.modal.removeAttribute('id');
937
- this.modal.customModalContent = nothing;
938
-
939
- // Recompute the true remaining time from the cache — not however
940
- // long the outage lasted — before resuming the stopped timers.
941
- const loanTime = await this.localCache.get(
942
- `${this.identifier}-loanTime`
943
- );
944
- const rawSecondsLeft = Math.round((loanTime - new Date()) / 1000);
945
- const secondsLeft =
946
- Number.isFinite(rawSecondsLeft) && rawSecondsLeft > 0
947
- ? rawSecondsLeft
948
- : this.loanRenewTimeConfig.loanTotalTime;
949
- this.lendingStatus = {
950
- ...this.lendingStatus,
951
- secondsLeftOnLoan: secondsLeft,
952
- };
953
- await this.startTimerCountdown();
954
- await this.startBrowseTimer();
955
- }
956
934
  };
957
935
  const errorCallback = eventObj => {
958
936
  this.handleLendingActionError(eventObj);
959
937
  };
960
938
 
961
- // Tear down any previous poller first — otherwise its own pending
962
- // internal retry timeout (see LoanTokenPoller.handleTokenError) is
963
- // never cancelled and can fire later against stale state.
939
+ // Tear down any previous poller first, so only one is ever controlling
940
+ // window.IALendingIntervals.tokenPoller at a time.
964
941
  this.tokenPoller?.disconnectedCallback();
965
- this.tokenPoller = new LoanTokenPoller(
966
- this.identifier,
967
- this.borrowType,
942
+ this.tokenPoller = new LoanTokenPoller({
943
+ identifier: this.identifier,
944
+ borrowType: this.borrowType,
968
945
  successCallback,
969
946
  errorCallback,
970
- this.tokenDelay // in seconds
971
- );
947
+ pollerDelay: this.tokenDelay, // in seconds
948
+ skipInitialCall,
949
+ });
972
950
  }
973
951
 
974
952
  /*
@@ -1005,38 +983,28 @@ export default class IABookActions extends LitElement {
1005
983
  });
1006
984
 
1007
985
  if (action === 'create_token') {
1008
- // A create_token hiccup says nothing about how much time is left on
1009
- // the loan, so it must not stop the countdown by itself.
1010
- window?.IALendingIntervals?.clearTokenPoller();
1011
-
1012
- // Only the INITIAL token matters enough to interrupt the patron —
1013
- // a routine interval refresh stays silent and just retries next tick.
986
+ // Only the INITIAL token matters enough to interrupt the patron — a
987
+ // routine interval refresh stays silent; the pages already loaded
988
+ // are still served, and the next scheduled poll tries again on its
989
+ // own without any special retry.
1014
990
  if (!isInitial) {
1015
991
  log(
1016
- '[IABookActions] create_token failed on interval refresh — staying silent, poller will retry'
992
+ '[IABookActions] create_token failed on interval refresh — staying silent'
1017
993
  );
1018
994
  return;
1019
995
  }
1020
996
 
1021
- // The loan itself is still active (do_renew succeeded) — don't touch
1022
- // lendingStatus, which would collapse borrowType to null and strand
1023
- // the poller from ever restarting. Still nothing to read right now
1024
- // though, so stop the countdown timers too; startLoanTokenPoller()'s
1025
- // successCallback resumes them once access recovers.
1026
- this.awaitingTokenRecovery = true;
1027
- window?.IALendingIntervals?.clearTimerCountdown();
1028
- window?.IALendingIntervals?.clearBrowseExpireTimeout();
1029
-
1030
- // Bounded: a genuinely-lost (not just slow-to-propagate) loan would
1031
- // otherwise retry this forever.
1032
- this.tokenRecoveryAttempts += 1;
1033
- if (this.tokenRecoveryAttempts <= this.maxTokenRecoveryAttempts) {
1034
- this.startLoanTokenPoller();
1035
- } else {
1036
- log('[IABookActions] create_token recovery attempts exhausted', {
1037
- identifier: this.identifier,
1038
- });
1039
- }
997
+ // The book never opened, and we don't retry — reset the bar to
998
+ // Borrow and the timer to 0 rather than leaving a red "Return now"
999
+ // with a stale countdown for a session that isn't accessible.
1000
+ window?.IALendingIntervals?.clearAll();
1001
+ this.tokenPoller?.disconnectedCallback();
1002
+ this.lendingStatus = {
1003
+ ...this.lendingStatus,
1004
+ user_has_browsed: false,
1005
+ available_to_browse: true,
1006
+ secondsLeftOnLoan: 0,
1007
+ };
1040
1008
 
1041
1009
  // showErrorModal has dedicated create_token messaging (refresh
1042
1010
  // button + support email).
@@ -1,4 +1,4 @@
1
- import { expect, aTimeout } from '@open-wc/testing';
1
+ import { expect } 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
- // id, borrowType, successCallback, errorCallback, pollerDelay
21
- const tokenPoller = new LoanTokenPoller(
22
- 'identifier1',
23
- 'browsed',
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();
@@ -57,144 +56,99 @@ describe('Get Loan Token', () => {
57
56
  });
58
57
  });
59
58
 
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
-
59
+ describe('handleTokenError (no retry)', () => {
60
+ // A create_token failure is treated as terminal on the first attempt —
61
+ // no retry, whether it's the "stale loan read" race or anything else.
69
62
  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', () => {
63
+ new LoanTokenPoller({
64
+ identifier: 'identifier1',
65
+ borrowType: 'browsed',
66
+ successCallback: () => {},
67
+ errorCallback: () => {},
68
+ pollerDelay: 2000,
69
+ });
70
+
71
+ it('reports the error immediately, without retrying, on the initial call', () => {
79
72
  const tokenPoller = makeTokenPoller();
80
73
  const handleLoanTokenPollerSpy = Sinon.spy(
81
74
  tokenPoller,
82
75
  'handleLoanTokenPoller'
83
76
  );
84
77
  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.tokenRetryDelay);
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
78
 
104
79
  tokenPoller.handleTokenError(
105
- staleLoanReadError,
106
- true,
107
- tokenPoller.maxTokenRetries
80
+ { error: 'You do not currently have this book borrowed.' },
81
+ true
108
82
  );
109
83
 
110
84
  expect(errorCallbackSpy.calledOnce).to.be.true;
85
+ expect(handleLoanTokenPollerSpy.called).to.be.false;
111
86
  });
112
87
 
113
- it('retries a routine (non-initial) poll on the same error too', () => {
114
- // A routine tick landing moments after a renewal hits the very same
115
- // write-propagation race as the initial call, so it's equally worth
116
- // retrying. isInitial only decides how a FINAL failure is reported.
117
- const tokenPoller = makeTokenPoller();
118
- const handleLoanTokenPollerSpy = Sinon.spy(
119
- tokenPoller,
120
- 'handleLoanTokenPoller'
121
- );
122
- const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
123
- const clock = Sinon.useFakeTimers();
124
-
125
- try {
126
- tokenPoller.handleTokenError(staleLoanReadError, false, 0);
127
-
128
- expect(errorCallbackSpy.called).to.be.false;
129
-
130
- clock.tick(tokenPoller.tokenRetryDelay);
131
-
132
- expect(handleLoanTokenPollerSpy.calledWith(false, 1)).to.be.true;
133
- } finally {
134
- clock.restore();
135
- }
136
- });
137
-
138
- it('does not retry a genuinely different error, even on the initial call', () => {
88
+ it('reports the error immediately on a routine (non-initial) poll too', () => {
139
89
  const tokenPoller = makeTokenPoller();
140
90
  const errorCallbackSpy = Sinon.spy(tokenPoller, 'errorCallback');
141
91
 
142
92
  tokenPoller.handleTokenError(
143
- { error: 'loan token not found. please try again later.' },
144
- true,
145
- 0
93
+ { error: 'You do not currently have this book borrowed.' },
94
+ false
146
95
  );
147
96
 
148
97
  expect(errorCallbackSpy.calledOnce).to.be.true;
149
98
  });
150
- });
151
-
152
- describe('LoanTokenPoller - WEBDEV-8322 review fixes', () => {
153
- it('retries a stale-loan-read error on interval refreshes too, not just the initial call', async () => {
154
- const poller = new LoanTokenPoller('foo', 'browsed', () => {}, () => {}, 120);
155
- poller.disconnectedCallback(); // stop the real poller started in the ctor
156
-
157
- const retrySpy = Sinon.stub(poller, 'handleLoanTokenPoller');
158
- poller.tokenRetryDelay = 1;
159
-
160
- poller.handleTokenError(
161
- { error: 'you do not currently have this book borrowed' },
162
- false, // interval refresh
163
- 0
164
- );
165
- await aTimeout(30);
166
-
167
- expect(retrySpy.calledOnce).to.be.true;
168
- expect(retrySpy.firstCall.args).to.deep.equal([false, 1]);
169
- poller.disconnectedCallback();
170
- });
171
99
 
172
100
  it('passes isInitial through to the error callback', () => {
173
101
  const errorCallback = Sinon.spy();
174
- const poller = new LoanTokenPoller('foo', 'browsed', () => {}, errorCallback, 120);
102
+ const poller = new LoanTokenPoller({
103
+ identifier: 'foo',
104
+ borrowType: 'browsed',
105
+ successCallback: () => {},
106
+ errorCallback,
107
+ pollerDelay: 120,
108
+ });
175
109
  poller.disconnectedCallback();
176
110
 
177
- poller.handleTokenError({ error: 'something else went wrong' }, true, 0);
111
+ poller.handleTokenError({ error: 'something else went wrong' }, true);
178
112
 
179
113
  expect(errorCallback.calledOnce).to.be.true;
180
114
  expect(errorCallback.firstCall.args[0].detail.isInitial).to.be.true;
181
115
  });
116
+ });
182
117
 
183
- it('cancels a pending retry on teardown', async () => {
184
- const poller = new LoanTokenPoller('foo', 'browsed', () => {}, () => {}, 120);
185
- const retrySpy = Sinon.stub(poller, 'handleLoanTokenPoller');
186
- poller.tokenRetryDelay = 20;
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
+ });
187
138
 
188
- poller.handleTokenError(
189
- { error: 'you do not currently have this book borrowed' },
190
- true,
191
- 0
192
- );
193
- expect(poller.retryTimeout).to.not.be.undefined;
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');
194
148
 
195
- poller.disconnectedCallback();
196
- await aTimeout(60);
149
+ poller.bookAccessed();
197
150
 
198
- expect(retrySpy.called).to.be.false;
151
+ expect(handleLoanTokenPollerSpy.calledWith(true)).to.be.true;
152
+ poller.disconnectedCallback();
199
153
  });
200
154
  });
@@ -479,8 +479,7 @@ describe('Browsing expired status', () => {
479
479
  });
480
480
  });
481
481
 
482
- describe('Auto renew one hour loan', async () => {
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', async () => {
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
  );
@@ -755,35 +758,6 @@ describe('autoRenewExpiredLoan', () => {
755
758
  expect(el.loanRenewResult.renewNow).to.be.true;
756
759
  });
757
760
 
758
- it('does not start a new renewal while awaiting create_token recovery from the last one', async () => {
759
- // loanRenewInProgress clears as soon as renew_loan itself succeeds
760
- // (see handleLoanAutoRenewed), before create_token is known to work.
761
- // A visibilitychange/userAction landing in that window must not kick
762
- // off ANOTHER renewal — that repeats the do_renew() delete+create
763
- // write for no reason. See WEBDEV-8322 production log, 2026-09-28.
764
- const el = await fixture(
765
- container({
766
- userid: '@user1',
767
- identifier: 'foobar',
768
- lendingStatus: {
769
- user_has_browsed: true,
770
- browsingExpired: true,
771
- },
772
- })
773
- );
774
- await el.updateComplete;
775
-
776
- el.loanRenewInProgress = false;
777
- el.awaitingTokenRecovery = true;
778
-
779
- const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
780
- el.autoRenewExpiredLoan();
781
-
782
- expect(spy.callCount).to.equal(1);
783
- expect(el.loanRenewInProgress).to.be.false;
784
- expect(el.loanRenewResult.renewNow).to.be.false;
785
- });
786
-
787
761
  it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
788
762
  const el = await fixture(
789
763
  container({
@@ -969,6 +943,100 @@ describe('autoRenewExpiredLoan', () => {
969
943
  expect(el.lendingBarPostInit.called).to.be.false;
970
944
  });
971
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
+
962
+ await el.localCache.set({
963
+ key: 'foobar-loanTime',
964
+ value: new Date(new Date().getTime() + 3600 * 1000),
965
+ ttl: 3600,
966
+ });
967
+
968
+ const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
969
+
970
+ // Triggers updated()'s clearAll(), stopping the running poller, same
971
+ // as a real routine top-up would.
972
+ el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
973
+ await el.updateComplete;
974
+
975
+ const collapsibleActionGroupEl = el.shadowRoot.querySelector(
976
+ 'collapsible-action-group'
977
+ );
978
+ collapsibleActionGroupEl.dispatchEvent(
979
+ new CustomEvent('loanAutoRenewed', {
980
+ detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
981
+ })
982
+ );
983
+
984
+ await aTimeout(200);
985
+ await el.updateComplete;
986
+
987
+ expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
988
+ });
989
+
990
+ it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
991
+ // BookLoanService::attempt_to_renew_loan() now mints the access token
992
+ // as part of the renew_loan response itself (using the just-written
993
+ // loan record already in memory, not a separate read) -- so recovering
994
+ // from a genuine expiry no longer needs to wait on a confirming
995
+ // create_token success before re-initializing BookReader, and the
996
+ // poller it starts afterward shouldn't fire a redundant immediate call.
997
+ const el = await fixture(
998
+ container({
999
+ userid: '@user1',
1000
+ identifier: 'foobar',
1001
+ lendingStatus: {
1002
+ user_has_browsed: true,
1003
+ browsingExpired: true,
1004
+ },
1005
+ })
1006
+ );
1007
+ await el.updateComplete;
1008
+ el.lendingBarPostInit = Sinon.spy();
1009
+
1010
+ await el.localCache.set({
1011
+ key: 'foobar-loanTime',
1012
+ value: new Date(new Date().getTime() + 3600 * 1000),
1013
+ ttl: 3600,
1014
+ });
1015
+
1016
+ const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
1017
+
1018
+ el.autoRenewExpiredLoan();
1019
+ await el.updateComplete;
1020
+ await aTimeout(50); // let autoRenewExpiredLoan's setTimeout(0) set renewNow
1021
+
1022
+ await el.handleLoanAutoRenewed({
1023
+ detail: {
1024
+ action: 'renew_loan',
1025
+ data: { success: true, loan: { identifier: 'foobar', renewal: true } },
1026
+ },
1027
+ });
1028
+
1029
+ // Called synchronously from handleLoanAutoRenewed, not via a
1030
+ // create_token success callback.
1031
+ expect(el.lendingBarPostInit.calledOnce).to.be.true;
1032
+ expect(el.postInitComplete).to.be.true;
1033
+
1034
+ await el.updateComplete;
1035
+ await aTimeout(150); // let the 100ms token-poller-restart check run
1036
+
1037
+ expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
1038
+ });
1039
+
972
1040
  it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
973
1041
  const el = await fixture(
974
1042
  container({
@@ -1130,14 +1198,11 @@ describe('handleLendingActionError - create_token failures must not stop the cou
1130
1198
  expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1131
1199
  });
1132
1200
 
1133
- it('shows the error modal for a create_token failure that reaches here (retries already exhausted)', async () => {
1134
- // By the time handleLendingActionError runs for a create_token
1135
- // failure, LoanTokenPoller has already retried and given up (see
1136
- // loan-token-poller.js's handleTokenError). For the INITIAL token
1137
- // that's terminal — BookReader can't be initialized and the book
1138
- // won't open — so the patron must be told rather than left staring
1139
- // at nothing. (An interval refresh failing is handled separately and
1140
- // stays silent; see the WEBDEV-8322 review fixes below.)
1201
+ it('shows the error modal and resets to Borrow/0 on a failed INITIAL create_token (no retry)', async () => {
1202
+ // For the INITIAL token, BookReader never got page images — the book
1203
+ // never opened. We don't retry, so the bar must reflect that honestly
1204
+ // (Borrow, 0 left) instead of staying red with a stale countdown for a
1205
+ // session that isn't actually accessible.
1141
1206
  const el = await fixture(
1142
1207
  container({
1143
1208
  userid: '@user1',
@@ -1164,24 +1229,16 @@ describe('handleLendingActionError - create_token failures must not stop the cou
1164
1229
 
1165
1230
  expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1166
1231
  .true;
1167
- // The loan itself is still active (do_renew succeeded) — flipping this
1168
- // to false used to collapse borrowType to null in
1169
- // getCurrentLendingActions(), which made setupLendingToolbarActions()
1170
- // bail out before ever restarting the token poller, silently
1171
- // stranding the patron until the countdown force-returned the book.
1172
- // See WEBDEV-8322 production log, 2026-09-28.
1173
- expect(el.lendingStatus.user_has_browsed).to.be.true;
1174
- expect(el.awaitingTokenRecovery).to.be.true;
1232
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1233
+ expect(el.lendingStatus.available_to_browse).to.be.true;
1234
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1175
1235
  });
1176
1236
 
1177
1237
  it('stops the countdown/expiry timers on a failed INITIAL create_token — nothing to read yet', async () => {
1178
1238
  // Unlike the interval-refresh case above (pages already loaded, keep
1179
- // ticking), an initial create_token failure means BookReader never
1180
- // got page images — there's nothing to read. Letting the countdown
1181
- // keep running just re-syncs on every single tick against a broken
1182
- // session (see WEBDEV-8322 QA log, 2026-09-28) until the loan
1183
- // eventually force-expires. Stop both timers instead; they resume
1184
- // once startLoanTokenPoller()'s successCallback confirms recovery.
1239
+ // ticking), an initial create_token failure means BookReader never got
1240
+ // page images and we're resetting the bar to Borrow — both timers
1241
+ // should stop rather than keep counting down a dead session.
1185
1242
  const el = await fixture(
1186
1243
  container({
1187
1244
  userid: '@user1',
@@ -1582,70 +1639,6 @@ describe('WEBDEV-8322 review fixes', () => {
1582
1639
  expect(el.lendingStatus.user_has_browsed).to.be.true;
1583
1640
  });
1584
1641
 
1585
- it('surfaces a failed initial create_token, which blocks opening the book', async () => {
1586
- const el = await fixture(
1587
- container({
1588
- userid: '@user1',
1589
- identifier: 'token-initial',
1590
- lendingStatus: {
1591
- is_lendable: true,
1592
- available_to_browse: true,
1593
- user_has_browsed: true,
1594
- browsingExpired: false,
1595
- secondsLeftOnLoan: 300,
1596
- },
1597
- })
1598
- );
1599
- await el.updateComplete;
1600
- const spy = Sinon.spy(el, 'showErrorModal');
1601
-
1602
- el.handleLendingActionError({
1603
- detail: {
1604
- action: 'create_token',
1605
- isInitial: true,
1606
- data: { error: 'loan token not found. please try again later.' },
1607
- },
1608
- });
1609
- await el.updateComplete;
1610
-
1611
- expect(spy.calledOnce).to.be.true;
1612
- // Still reading — see the identical assertion above for why this must
1613
- // stay true (WEBDEV-8322 production log, 2026-09-28).
1614
- expect(el.lendingStatus.user_has_browsed).to.be.true;
1615
- expect(el.awaitingTokenRecovery).to.be.true;
1616
- });
1617
-
1618
- it('does not resume the countdown via visibilitychange while awaiting create_token recovery', async () => {
1619
- // Regression: visibilitychange's resync path didn't check
1620
- // awaitingTokenRecovery, so backgrounding/foregrounding the tab during
1621
- // a create_token outage could resume the timers that
1622
- // handleLendingActionError had deliberately stopped, and auto-return a
1623
- // loan the server still considers valid.
1624
- const el = await fixture(
1625
- container({
1626
- userid: '@user1',
1627
- identifier: 'foobar',
1628
- lendingStatus: {
1629
- user_has_browsed: true,
1630
- browsingExpired: false,
1631
- secondsLeftOnLoan: 300,
1632
- },
1633
- })
1634
- );
1635
- await el.updateComplete;
1636
- el.awaitingTokenRecovery = true;
1637
-
1638
- const spy = Sinon.spy(el, 'loanStatusCheckInterval');
1639
- Object.defineProperty(document, 'hidden', {
1640
- value: false,
1641
- configurable: true,
1642
- });
1643
- document.dispatchEvent(new Event('visibilitychange'));
1644
- await aTimeout(100);
1645
-
1646
- expect(spy.called).to.be.false;
1647
- });
1648
-
1649
1642
  it('tears down the previous poller before starting a new one', async () => {
1650
1643
  // Regression: startLoanTokenPoller() always replaced this.tokenPoller
1651
1644
  // without cancelling the old instance's pending internal retry timer,
@@ -1675,9 +1668,7 @@ describe('WEBDEV-8322 review fixes', () => {
1675
1668
  expect(el.tokenPoller).to.not.equal(firstPoller);
1676
1669
  });
1677
1670
 
1678
- it('stops restarting the poller after maxTokenRecoveryAttempts consecutive create_token failures', async () => {
1679
- // Regression: an outer restart loop with no cap meant a genuinely-lost
1680
- // (not just slow-to-propagate) loan retried forever.
1671
+ it('never restarts the poller on a create_token failure — no retry', async () => {
1681
1672
  const el = await fixture(
1682
1673
  container({
1683
1674
  userid: '@user1',
@@ -1694,19 +1685,15 @@ describe('WEBDEV-8322 review fixes', () => {
1694
1685
  await el.updateComplete;
1695
1686
 
1696
1687
  const startSpy = Sinon.spy(el, 'startLoanTokenPoller');
1697
- const errorMsg = 'loan token not found. please try again later.';
1698
1688
 
1699
- for (let i = 0; i < el.maxTokenRecoveryAttempts + 1; i += 1) {
1700
- el.handleLendingActionError({
1701
- detail: {
1702
- action: 'create_token',
1703
- isInitial: true,
1704
- data: { error: errorMsg },
1705
- },
1706
- });
1707
- }
1689
+ el.handleLendingActionError({
1690
+ detail: {
1691
+ action: 'create_token',
1692
+ isInitial: true,
1693
+ data: { error: 'loan token not found. please try again later.' },
1694
+ },
1695
+ });
1708
1696
 
1709
- expect(startSpy.callCount).to.equal(el.maxTokenRecoveryAttempts);
1710
- expect(el.tokenRecoveryAttempts).to.equal(el.maxTokenRecoveryAttempts + 1);
1697
+ expect(startSpy.called).to.be.false;
1711
1698
  });
1712
1699
  });