@internetarchive/ia-book-actions 1.4.4-alpha9 → 1.4.5

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.
@@ -121,6 +121,84 @@ describe('<ia-book-actions>', () => {
121
121
 
122
122
  expect(spy.calledOnce).to.be.true;
123
123
  });
124
+
125
+ it('ignores BookReader:userAction fired by BookReader init (WEBDEV-8322 follow-up)', async () => {
126
+ // Regression: BookReader.updateFirstIndex() fires 'userAction'
127
+ // unconditionally — including on BookReader's own init-time jump to
128
+ // the reader's last-read page (updateFromParams(), called from
129
+ // init()), which has nothing to do with the patron doing anything.
130
+ // Auto-renewing off that would silently re-borrow a lapsed book on a
131
+ // plain page refresh, before the patron did anything at all.
132
+ //
133
+ // BookReader.trigger() passes the BookReader instance as
134
+ // detail.props, and init() holds init.initComplete === false for
135
+ // exactly the span containing that jump.
136
+ const el = await fixture(
137
+ container({
138
+ userid: '@user1',
139
+ identifier: 'foobar',
140
+ lendingStatus: { user_has_browsed: true, browsingExpired: true },
141
+ })
142
+ );
143
+ el.borrowType = 'browsed';
144
+ await el.updateComplete;
145
+
146
+ const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
147
+
148
+ window.dispatchEvent(
149
+ new CustomEvent('BookReader:userAction', {
150
+ detail: { props: { init: { initComplete: false } } },
151
+ })
152
+ );
153
+ await el.updateComplete;
154
+
155
+ expect(spy.called).to.be.false;
156
+ });
157
+
158
+ it('honours BookReader:userAction once BookReader init has completed', async () => {
159
+ const el = await fixture(
160
+ container({
161
+ userid: '@user1',
162
+ identifier: 'foobar',
163
+ lendingStatus: { user_has_browsed: true, browsingExpired: true },
164
+ })
165
+ );
166
+ el.borrowType = 'browsed';
167
+ await el.updateComplete;
168
+
169
+ const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
170
+
171
+ window.dispatchEvent(
172
+ new CustomEvent('BookReader:userAction', {
173
+ detail: { props: { init: { initComplete: true } } },
174
+ })
175
+ );
176
+ await el.updateComplete;
177
+
178
+ expect(spy.calledOnce).to.be.true;
179
+ });
180
+
181
+ it('treats a userAction with no BookReader detail as a genuine interaction', async () => {
182
+ // A BookReader too old to expose init.initComplete, or any synthetic
183
+ // dispatch, must not be silently swallowed — only an explicit false
184
+ // suppresses the event.
185
+ const el = await fixture(
186
+ container({
187
+ userid: '@user1',
188
+ identifier: 'foobar',
189
+ lendingStatus: { user_has_browsed: true, browsingExpired: true },
190
+ })
191
+ );
192
+ el.borrowType = 'browsed';
193
+ await el.updateComplete;
194
+
195
+ const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
196
+
197
+ window.dispatchEvent(new Event('BookReader:userAction'));
198
+ await el.updateComplete;
199
+
200
+ expect(spy.calledOnce).to.be.true;
201
+ });
124
202
  });
125
203
 
126
204
  describe('Primary Actions data', () => {
@@ -401,8 +479,7 @@ describe('Browsing expired status', () => {
401
479
  });
402
480
  });
403
481
 
404
- describe('Auto renew one hour loan', async () => {
405
- await aTimeout(5000);
482
+ describe('Auto renew one hour loan', () => {
406
483
  it('Book is browsing and renewed it now', async () => {
407
484
  const loanTime = 88;
408
485
  const el = await fixture(
@@ -475,8 +552,7 @@ describe('Auto renew one hour loan', async () => {
475
552
  });
476
553
  });
477
554
 
478
- describe('Visibility change API for document', async () => {
479
- await aTimeout(5000);
555
+ describe('Visibility change API for document', () => {
480
556
 
481
557
  beforeEach(() => {
482
558
  Object.defineProperty(document, 'hidden', {
@@ -500,6 +576,10 @@ describe('Visibility change API for document', async () => {
500
576
  lendingStatus: {
501
577
  user_has_browsed: true,
502
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,
503
583
  },
504
584
  })
505
585
  );
@@ -527,6 +607,7 @@ describe('Visibility change API for document', async () => {
527
607
  lendingStatus: {
528
608
  user_has_browsed: true,
529
609
  browsingExpired: false,
610
+ secondsLeftOnLoan: 3600,
530
611
  },
531
612
  })
532
613
  );
@@ -595,6 +676,36 @@ describe('autoRenewExpiredLoan', () => {
595
676
  expect(el.primaryColor).to.equal('danger');
596
677
  });
597
678
 
679
+ it('does not (re)start the countdown during the optimistic pre-confirmation window (WEBDEV-8322 follow-up)', async () => {
680
+ // Regression: the optimistic browsingExpired flip above triggers
681
+ // setupLendingToolbarActions() before renew_loan is even dispatched,
682
+ // let alone confirmed. Without gating on loanRenewInProgress, that
683
+ // would (re)start the timer-countdown interval using whatever stale
684
+ // secondsLeftOnLoan was left over from before the loan expired,
685
+ // showing a wrong/flickering value until handleLoanAutoRenewed()
686
+ // corrects it moments later.
687
+ const el = await fixture(
688
+ container({
689
+ userid: '@user1',
690
+ identifier: 'foobar',
691
+ lendingStatus: {
692
+ user_has_browsed: true,
693
+ available_to_browse: true,
694
+ browsingExpired: true,
695
+ },
696
+ })
697
+ );
698
+ await el.updateComplete;
699
+
700
+ const startTimerCountdownSpy = Sinon.spy(el, 'startTimerCountdown');
701
+
702
+ el.autoRenewExpiredLoan();
703
+ await el.updateComplete;
704
+
705
+ expect(el.loanRenewInProgress).to.be.true;
706
+ expect(startTimerCountdownSpy.called).to.be.false;
707
+ });
708
+
598
709
  it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
599
710
  const el = await fixture(
600
711
  container({
@@ -660,9 +771,6 @@ describe('autoRenewExpiredLoan', () => {
660
771
  })
661
772
  );
662
773
  await el.updateComplete;
663
- // Not testing the post-confirmation token-poller delay here — avoid
664
- // leaving a real multi-second timer dangling past this test.
665
- el.tokenPollerRenewalDelay = 0;
666
774
 
667
775
  await el.localCache.set({
668
776
  key: 'foobar-loanTime',
@@ -682,7 +790,7 @@ describe('autoRenewExpiredLoan', () => {
682
790
  );
683
791
  collapsibleActionGroupEl.dispatchEvent(
684
792
  new CustomEvent('loanAutoRenewed', {
685
- detail: { data: { loan: { identifier: 'foobar' } } },
793
+ detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
686
794
  })
687
795
  );
688
796
 
@@ -712,9 +820,6 @@ describe('autoRenewExpiredLoan', () => {
712
820
  await el.updateComplete;
713
821
  el.postInitComplete = true;
714
822
  el.lendingBarPostInit = Sinon.spy();
715
- // Use a small but non-zero delay to keep this test fast while still
716
- // exercising the same "wait, then restart the poller" code path.
717
- el.tokenPollerRenewalDelay = 50;
718
823
 
719
824
  await el.localCache.set({
720
825
  key: 'foobar-loanTime',
@@ -736,14 +841,11 @@ describe('autoRenewExpiredLoan', () => {
736
841
  );
737
842
  collapsibleActionGroupEl.dispatchEvent(
738
843
  new CustomEvent('loanAutoRenewed', {
739
- detail: { data: { loan: { identifier: 'foobar' } } },
844
+ detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
740
845
  })
741
846
  );
742
847
 
743
- // The post-confirmation token-poller restart is deliberately delayed
744
- // (datanode write-propagation buffer — see setupLendingToolbarActions)
745
- // before the recovery case's first create_token call.
746
- await aTimeout(250);
848
+ await aTimeout(200);
747
849
  await el.updateComplete;
748
850
 
749
851
  // The confirmed renewal must have forced a fresh create_token cycle
@@ -781,10 +883,6 @@ describe('autoRenewExpiredLoan', () => {
781
883
  ttl: 3600,
782
884
  });
783
885
 
784
- // Use a small but non-zero delay to keep this test fast while still
785
- // exercising the same "wait, then restart the poller" code path.
786
- el.tokenPollerRenewalDelay = 50;
787
-
788
886
  const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
789
887
 
790
888
  el.autoRenewExpiredLoan();
@@ -799,13 +897,11 @@ describe('autoRenewExpiredLoan', () => {
799
897
  );
800
898
  collapsibleActionGroupEl.dispatchEvent(
801
899
  new CustomEvent('loanAutoRenewed', {
802
- detail: { data: { loan: { identifier: 'foobar' } } },
900
+ detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
803
901
  })
804
902
  );
805
903
 
806
- // The post-confirmation restart is deliberately delayed (datanode
807
- // write-propagation buffer — see setupLendingToolbarActions).
808
- await aTimeout(250);
904
+ await aTimeout(200);
809
905
  await el.updateComplete;
810
906
 
811
907
  expect(startTokenPollerSpy.calledOnce).to.be.true;
@@ -837,7 +933,7 @@ describe('autoRenewExpiredLoan', () => {
837
933
  );
838
934
  collapsibleActionGroupEl.dispatchEvent(
839
935
  new CustomEvent('loanAutoRenewed', {
840
- detail: { data: { loan: { identifier: 'foobar' } } },
936
+ detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
841
937
  })
842
938
  );
843
939
 
@@ -847,6 +943,105 @@ describe('autoRenewExpiredLoan', () => {
847
943
  expect(el.lendingBarPostInit.called).to.be.false;
848
944
  });
849
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
+ // Retries any page image broken during the gap this renewal just closed.
990
+ expect(el.reloadPageImages.called).to.be.true;
991
+ });
992
+
993
+ it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
994
+ // BookLoanService::attempt_to_renew_loan() now mints the access token
995
+ // as part of the renew_loan response itself (using the just-written
996
+ // loan record already in memory, not a separate read) -- so recovering
997
+ // from a genuine expiry no longer needs to wait on a confirming
998
+ // create_token success before re-initializing BookReader, and the
999
+ // poller it starts afterward shouldn't fire a redundant immediate call.
1000
+ const el = await fixture(
1001
+ container({
1002
+ userid: '@user1',
1003
+ identifier: 'foobar',
1004
+ lendingStatus: {
1005
+ user_has_browsed: true,
1006
+ browsingExpired: true,
1007
+ },
1008
+ })
1009
+ );
1010
+ await el.updateComplete;
1011
+ el.lendingBarPostInit = Sinon.spy();
1012
+ el.reloadPageImages = Sinon.spy();
1013
+
1014
+ await el.localCache.set({
1015
+ key: 'foobar-loanTime',
1016
+ value: new Date(new Date().getTime() + 3600 * 1000),
1017
+ ttl: 3600,
1018
+ });
1019
+
1020
+ const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
1021
+
1022
+ el.autoRenewExpiredLoan();
1023
+ await el.updateComplete;
1024
+ await aTimeout(50); // let autoRenewExpiredLoan's setTimeout(0) set renewNow
1025
+
1026
+ await el.handleLoanAutoRenewed({
1027
+ detail: {
1028
+ action: 'renew_loan',
1029
+ data: { success: true, loan: { identifier: 'foobar', renewal: true } },
1030
+ },
1031
+ });
1032
+
1033
+ // Called synchronously from handleLoanAutoRenewed, not via a
1034
+ // create_token success callback.
1035
+ expect(el.lendingBarPostInit.calledOnce).to.be.true;
1036
+ expect(el.postInitComplete).to.be.true;
1037
+ expect(el.reloadPageImages.called).to.be.true;
1038
+
1039
+ await el.updateComplete;
1040
+ await aTimeout(150); // let the 100ms token-poller-restart check run
1041
+
1042
+ expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
1043
+ });
1044
+
850
1045
  it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
851
1046
  const el = await fixture(
852
1047
  container({
@@ -980,8 +1175,75 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
980
1175
  });
981
1176
  });
982
1177
 
983
- describe('handleLendingActionError - create_token failures must not stop the countdown (WEBDEV-8322 follow-up)', () => {
984
- it('does not clear the reading countdown on a create_token failure', async () => {
1178
+ describe('handleLendingActionError - every create_token failure resets the bar (WEBDEV-8322 follow-up)', () => {
1179
+ // A create_token failure can't be told apart from "the loan was actually
1180
+ // returned" (e.g. a stale loan-record cache on another node, or a genuine
1181
+ // revokeAccess() in BookReaderImages.php) from here, so initial and
1182
+ // routine interval failures are treated identically: no retry, reset to
1183
+ // Borrow/0, show the modal every time.
1184
+ for (const isInitial of [true, false]) {
1185
+ it(`shows the error modal and resets to Borrow/0 on a failed create_token (isInitial=${isInitial})`, async () => {
1186
+ const el = await fixture(
1187
+ container({
1188
+ userid: '@user1',
1189
+ identifier: 'foobar',
1190
+ lendingStatus: {
1191
+ user_has_browsed: true,
1192
+ browsingExpired: false,
1193
+ secondsLeftOnLoan: 100,
1194
+ },
1195
+ })
1196
+ );
1197
+ await el.updateComplete;
1198
+
1199
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1200
+ const errorMsg = 'loan token not found. please try again later.';
1201
+
1202
+ el.handleLendingActionError({
1203
+ detail: {
1204
+ action: 'create_token',
1205
+ isInitial,
1206
+ data: { error: errorMsg },
1207
+ },
1208
+ });
1209
+
1210
+ expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1211
+ .true;
1212
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1213
+ expect(el.lendingStatus.available_to_browse).to.be.true;
1214
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1215
+ });
1216
+
1217
+ it(`stops the countdown/expiry timers on a failed create_token (isInitial=${isInitial})`, async () => {
1218
+ const el = await fixture(
1219
+ container({
1220
+ userid: '@user1',
1221
+ identifier: 'foobar',
1222
+ lendingStatus: {
1223
+ user_has_browsed: true,
1224
+ browsingExpired: false,
1225
+ secondsLeftOnLoan: 100,
1226
+ },
1227
+ })
1228
+ );
1229
+ await el.updateComplete;
1230
+ expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1231
+ expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1232
+
1233
+ el.handleLendingActionError({
1234
+ detail: {
1235
+ action: 'create_token',
1236
+ isInitial,
1237
+ data: { error: 'loan token not found. please try again later.' },
1238
+ },
1239
+ });
1240
+
1241
+ expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1242
+ expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1243
+ });
1244
+ }
1245
+
1246
+ it('shows the modal even without a specific error message', async () => {
985
1247
  const el = await fixture(
986
1248
  container({
987
1249
  userid: '@user1',
@@ -994,18 +1256,14 @@ describe('handleLendingActionError - create_token failures must not stop the cou
994
1256
  })
995
1257
  );
996
1258
  await el.updateComplete;
997
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1259
+
1260
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
998
1261
 
999
1262
  el.handleLendingActionError({
1000
- detail: {
1001
- action: 'create_token',
1002
- data: { error: 'loan token not found. please try again later.' },
1003
- },
1263
+ detail: { action: 'create_token', isInitial: false, data: {} },
1004
1264
  });
1005
1265
 
1006
- // The countdown must keep running — a token refresh hiccup says
1007
- // nothing about how much time is left on the loan itself.
1008
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1266
+ expect(showErrorModalSpy.calledOnce).to.be.true;
1009
1267
  });
1010
1268
 
1011
1269
  it('still clears everything on a renew_loan failure', async () => {
@@ -1231,3 +1489,213 @@ describe('Shared Resize Observer', () => {
1231
1489
  expect(component.sharedObserver).to.not.be.undefined;
1232
1490
  });
1233
1491
  });
1492
+
1493
+ describe('WEBDEV-8322 review fixes', () => {
1494
+ it('populates the action bar on a fresh load of an already-expired loan', async () => {
1495
+ // Regression: the hasExpired early-return was hoisted above the
1496
+ // GetLendingActions computation so auto-return wouldn't visibly change
1497
+ // the bar. That's right for the transition, but this method also runs
1498
+ // on the FIRST lendingStatus update, where there is no bar to preserve
1499
+ // — leaving the patron a blank bar and no way to re-borrow.
1500
+ const el = await fixture(
1501
+ container({
1502
+ userid: '@user1',
1503
+ identifier: 'fresh-expired',
1504
+ lendingStatus: {
1505
+ is_lendable: true,
1506
+ available_to_browse: true,
1507
+ available_to_borrow: true,
1508
+ user_has_browsed: true,
1509
+ browsingExpired: true,
1510
+ secondsLeftOnLoan: 0,
1511
+ },
1512
+ })
1513
+ );
1514
+ await el.updateComplete;
1515
+
1516
+ expect(el.primaryActions.length).to.be.greaterThan(0);
1517
+ expect(el.primaryActions.map(a => a.id)).to.include('browseBook');
1518
+ });
1519
+
1520
+ it('still leaves a rendered action bar untouched when the loan auto-returns', async () => {
1521
+ const el = await fixture(
1522
+ container({
1523
+ userid: '@user1',
1524
+ identifier: 'auto-returned',
1525
+ lendingStatus: {
1526
+ is_lendable: true,
1527
+ available_to_browse: true,
1528
+ available_to_borrow: true,
1529
+ user_has_browsed: true,
1530
+ browsingExpired: false,
1531
+ secondsLeftOnLoan: 300,
1532
+ },
1533
+ })
1534
+ );
1535
+ await el.updateComplete;
1536
+ expect(el.primaryActions[0].text).to.equal('Return now');
1537
+
1538
+ el.lendingStatus = { ...el.lendingStatus, browsingExpired: true };
1539
+ await el.updateComplete;
1540
+
1541
+ // Auto-return must not visibly change the bar (the ticket's core ask).
1542
+ expect(el.primaryActions[0].text).to.equal('Return now');
1543
+ });
1544
+
1545
+ it('does not treat a `renewal: false` response as a successful renewal', async () => {
1546
+ // Regression: handleLoanAutoRenewed saw a truthy `loan` and took the
1547
+ // success path. setBrowseTimeSession() never ran, so the loanTime read
1548
+ // was undefined -> NaN -> the fallback invented a full fresh hour,
1549
+ // putting a new countdown on screen next to the failure modal.
1550
+ const el = await fixture(
1551
+ container({
1552
+ userid: '@user1',
1553
+ identifier: 'not-renewed',
1554
+ lendingStatus: {
1555
+ is_lendable: true,
1556
+ available_to_browse: true,
1557
+ user_has_browsed: true,
1558
+ browsingExpired: true,
1559
+ secondsLeftOnLoan: 0,
1560
+ },
1561
+ })
1562
+ );
1563
+ await el.updateComplete;
1564
+ el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
1565
+ el.loanRenewInProgress = true;
1566
+ await el.updateComplete;
1567
+
1568
+ await el.handleLoanAutoRenewed({
1569
+ detail: {
1570
+ action: 'renew_loan',
1571
+ data: { success: true, loan: { renewal: false } },
1572
+ },
1573
+ });
1574
+ await el.updateComplete;
1575
+
1576
+ expect(el.lendingStatus.secondsLeftOnLoan).to.not.equal(
1577
+ el.loanRenewTimeConfig.loanTotalTime
1578
+ );
1579
+ expect(el.loanRenewInProgress).to.be.false;
1580
+ });
1581
+
1582
+ it('clears loanRenewInProgress when the renewal response carries no loan', async () => {
1583
+ // Regression: the !activeLoan branch cleared recoveringFromLoanExpiry
1584
+ // but left loanRenewInProgress set, latching the component — every
1585
+ // later renewal attempt no-ops and neither the countdown nor the token
1586
+ // poller ever restarts.
1587
+ const el = await fixture(
1588
+ container({
1589
+ userid: '@user1',
1590
+ identifier: 'no-loan',
1591
+ lendingStatus: { user_has_browsed: true, browsingExpired: true },
1592
+ })
1593
+ );
1594
+ await el.updateComplete;
1595
+
1596
+ el.autoRenewExpiredLoan();
1597
+ expect(el.loanRenewInProgress).to.be.true;
1598
+
1599
+ await el.handleLoanAutoRenewed({
1600
+ detail: { action: 'renew_loan', data: { success: true } },
1601
+ });
1602
+
1603
+ expect(el.loanRenewInProgress).to.be.false;
1604
+ expect(el.recoveringFromLoanExpiry).to.be.false;
1605
+
1606
+ // and the component is usable again, not latched
1607
+ el.autoRenewExpiredLoan();
1608
+ expect(el.loanRenewInProgress).to.be.true;
1609
+ });
1610
+
1611
+ it('resets the bar and shows the modal on an interval create_token refresh failure too', async () => {
1612
+ // A routine refresh failing is treated the same as the initial one --
1613
+ // there's no reliable way to tell "harmless blip" from "the loan is
1614
+ // actually gone" from here (see handleLendingActionError).
1615
+ const el = await fixture(
1616
+ container({
1617
+ userid: '@user1',
1618
+ identifier: 'token-blip',
1619
+ lendingStatus: {
1620
+ is_lendable: true,
1621
+ available_to_browse: true,
1622
+ user_has_browsed: true,
1623
+ browsingExpired: false,
1624
+ secondsLeftOnLoan: 300,
1625
+ },
1626
+ })
1627
+ );
1628
+ await el.updateComplete;
1629
+ const spy = Sinon.spy(el, 'showErrorModal');
1630
+
1631
+ el.handleLendingActionError({
1632
+ detail: {
1633
+ action: 'create_token',
1634
+ isInitial: false,
1635
+ data: { error: 'loan token not found. please try again later.' },
1636
+ },
1637
+ });
1638
+ await el.updateComplete;
1639
+
1640
+ expect(spy.calledOnce).to.be.true;
1641
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1642
+ });
1643
+
1644
+ it('tears down the previous poller before starting a new one', async () => {
1645
+ // Regression: startLoanTokenPoller() always replaced this.tokenPoller
1646
+ // without cancelling the old instance's pending internal retry timer,
1647
+ // which could later fire against stale state.
1648
+ const el = await fixture(
1649
+ container({
1650
+ userid: '@user1',
1651
+ identifier: 'poller-teardown',
1652
+ lendingStatus: {
1653
+ is_lendable: true,
1654
+ available_to_browse: true,
1655
+ user_has_browsed: true,
1656
+ browsingExpired: false,
1657
+ secondsLeftOnLoan: 300,
1658
+ },
1659
+ })
1660
+ );
1661
+ await el.updateComplete;
1662
+
1663
+ el.startLoanTokenPoller();
1664
+ const firstPoller = el.tokenPoller;
1665
+ const disconnectSpy = Sinon.spy(firstPoller, 'disconnectedCallback');
1666
+
1667
+ el.startLoanTokenPoller();
1668
+
1669
+ expect(disconnectSpy.calledOnce).to.be.true;
1670
+ expect(el.tokenPoller).to.not.equal(firstPoller);
1671
+ });
1672
+
1673
+ it('never restarts the poller on a create_token failure — no retry', async () => {
1674
+ const el = await fixture(
1675
+ container({
1676
+ userid: '@user1',
1677
+ identifier: 'token-exhausted',
1678
+ lendingStatus: {
1679
+ is_lendable: true,
1680
+ available_to_browse: true,
1681
+ user_has_browsed: true,
1682
+ browsingExpired: false,
1683
+ secondsLeftOnLoan: 300,
1684
+ },
1685
+ })
1686
+ );
1687
+ await el.updateComplete;
1688
+
1689
+ const startSpy = Sinon.spy(el, 'startLoanTokenPoller');
1690
+
1691
+ el.handleLendingActionError({
1692
+ detail: {
1693
+ action: 'create_token',
1694
+ isInitial: true,
1695
+ data: { error: 'loan token not found. please try again later.' },
1696
+ },
1697
+ });
1698
+
1699
+ expect(startSpy.called).to.be.false;
1700
+ });
1701
+ });
@@ -1,12 +0,0 @@
1
- import { html } from 'lit';
2
-
3
- export const infoIcon = html`<svg
4
- height="20"
5
- width="20"
6
- viewBox="0 0 100 100"
7
- xmlns="http://www.w3.org/2000/svg">
8
- <g fill="none" fill-rule="evenodd"><path d="m0 0h100v100h-100z" fill="#fff"/>
9
- <path d="m49.8315487 0h.1702245c6.7356878 0 13.1853038 1.31117332 19.3488483 3.93351997 6.1635444 2.62234664 11.4854233 6.15778963 15.9656369 10.60632903 4.4802135 4.4485394 8.0478347 9.7522946 10.7028636 15.9112655 2.655029 6.1589709 3.980878 12.6038012 3.9789971 19.3344909.0567419 6.6716279-1.1702933 13.0585776-3.6811042 19.1608491-2.510811 6.1022715-6.106803 11.5206067-10.7879759 16.2550055-9.7027949 9.7522946-21.4884754 14.6851412-35.3570414 14.79854h-.1702244c-6.7333236 0-13.1829397-1.3111733-19.3488483-3.93352-6.1659087-2.6223466-11.4877876-6.1577896-15.9656369-10.606329s-8.04547055-9.7522946-10.7028637-15.9112655c-2.65739314-6.1589709-3.9844243-12.6038012-3.98254337-19.3344909-.05674149-6.6716279 1.17029325-13.0585776 3.68110421-19.1608491 2.51081095-6.1022715 6.10680292-11.5206067 10.78797586-16.2550055 9.7027949-9.75229456 21.4884754-14.68514123 35.3570414-14.79854zm12.6566146 26.4757998c1.6745578-1.6828001 2.5118367-3.6747334 2.5118367-5.9757998 0-2.4126333-.8095238-4.4324583-2.4285714-6.059475s-3.6289796-2.440525-6.0297959-2.440525c-2.4008164 0-4.4107483.8135083-6.029796 2.440525-1.6745578 1.6270167-2.5118367 3.6468417-2.5118367 6.059475 0 2.1871753.8372789 4.1791086 2.5118367 5.9757998 1.6745579 1.6828001 3.6844898 2.5242002 6.029796 2.5242002 2.3453061 0 4.3274829-.8414001 5.9465306-2.5242002zm-12.1370589 52.7776981-1.2815282-.9486968c0-.4588935.398855-1.8938272 1.196565-4.3048011l12.7338588-39-23.0745876 3.6164609.2548896 3.873251c0-.1141289.4554971-.1997256 1.3664914-.2567901.9109942 0 1.623741.1723823 2.1382404.5171468.5121392.2306356.7965299.6039323.8531721 1.1198902 0 .8607225-.6549247 3.2134431-1.9647739 7.0581619l-8.1175252 24.1061729c-1.0242785 3.2716963-1.5080967 5.5388203-1.4514546 6.8013717.0566421 1.6643804.8826732 2.9839963 2.4780932 3.9588477 1.2532071.803658 2.8769482 1.205487 4.8712231 1.205487h.5982825c1.7653464-.0570645 3.8445846-.3875629 6.2377146-.9914952 2.3931299-.6039323 4.3590839-1.3933242 5.8978617-2.3681756 1.3098493-.803658 2.3919499-1.5359853 3.2463021-2.1969821.8543521-.6609968 1.3959925-1.1341564 1.6249211-1.4194788l.3433929-.3459533-2.8214861-3.3596708-2.9914125 2.0650205c-.79771.4588935-1.5104568.7454047-2.1382404.8595337z" fill="#000" fill-rule="nonzero"/>
10
- </g>
11
- </svg>
12
- `;