@internetarchive/ia-book-actions 1.4.2 → 1.4.3-alpha1

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