@internetarchive/ia-book-actions 1.4.1 → 1.4.3-canary.0
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/.github/workflows/ci.yml +8 -7
- package/.github/workflows/pr-preview.yml +28 -5
- package/README.md +11 -2
- package/package.json +15 -12
- package/src/core/services/loan-analytics.js +1 -1
- package/src/core/services/loan-renew-helper.js +1 -1
- package/src/ia-book-actions.js +132 -85
- package/test/ia-book-actions.test.js +215 -5
- package/web-test-runner.config.mjs +1 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -11,16 +11,17 @@ jobs:
|
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v4.1.1
|
|
15
|
+
- uses: actions/setup-node@v4.0.2
|
|
16
16
|
with:
|
|
17
|
-
node-version:
|
|
17
|
+
node-version: 20.x
|
|
18
18
|
|
|
19
19
|
- name: Install dependencies
|
|
20
20
|
run: yarn install
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: yarn run test
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
- uses: codecov/codecov-action@v5
|
|
26
|
+
with:
|
|
27
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -14,24 +14,47 @@ on:
|
|
|
14
14
|
|
|
15
15
|
concurrency: preview-${{ github.ref }}
|
|
16
16
|
|
|
17
|
+
env:
|
|
18
|
+
PREVIEW_BRANCH: gh-pages
|
|
17
19
|
jobs:
|
|
18
20
|
deploy-preview:
|
|
19
|
-
runs-on: ubuntu-
|
|
21
|
+
runs-on: ubuntu-latest
|
|
20
22
|
steps:
|
|
21
23
|
- name: Checkout
|
|
22
24
|
uses: actions/checkout@v4
|
|
23
|
-
|
|
24
|
-
- uses: actions/setup-node@v3
|
|
25
|
-
with:
|
|
26
|
-
node-version: 20.x
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
27
26
|
|
|
28
27
|
- name: Install and Build
|
|
29
28
|
run: |
|
|
30
29
|
yarn install
|
|
31
30
|
yarn prepare:ghpages
|
|
31
|
+
|
|
32
32
|
# Reference: https://github.com/rossjrw/pr-preview-action
|
|
33
33
|
- name: Deploy preview
|
|
34
34
|
uses: rossjrw/pr-preview-action@v1
|
|
35
|
+
id: preview-step
|
|
35
36
|
with:
|
|
36
37
|
source-dir: ./ghpages/
|
|
37
38
|
umbrella-dir: pr
|
|
39
|
+
preview-branch: ${{ env.PREVIEW_BRANCH }}
|
|
40
|
+
comment: false
|
|
41
|
+
|
|
42
|
+
- uses: marocchino/sticky-pull-request-comment@v2
|
|
43
|
+
if: steps.preview-step.outputs.deployment-action == 'deploy'
|
|
44
|
+
with:
|
|
45
|
+
header: pr-preview
|
|
46
|
+
message: |
|
|
47
|
+
[PR Preview Action](https://github.com/rossjrw/pr-preview-action) ${{ steps.preview-step.outputs.action-version }}
|
|
48
|
+
:---:
|
|
49
|
+
| <p></p> :rocket: View preview at <br> ${{ steps.preview-step.outputs.preview-url }} <br><br>
|
|
50
|
+
| <h6>Built to branch [`${{ env.PREVIEW_BRANCH }}`](${{ github.server_url }}/${{ github.repository }}/tree/${{ env.PREVIEW_BRANCH }}) at ${{ steps.preview-step.outputs.action-start-time }}. <br> Preview will be ready when the [GitHub Pages deployment](${{ github.server_url }}/${{ github.repository }}/deployments) is complete. <br><br> </h6>
|
|
51
|
+
|
|
52
|
+
- uses: marocchino/sticky-pull-request-comment@v2
|
|
53
|
+
if: steps.preview-step.outputs.deployment-action == 'remove'
|
|
54
|
+
with:
|
|
55
|
+
header: pr-preview
|
|
56
|
+
message: |
|
|
57
|
+
[PR Preview Action](https://github.com/rossjrw/pr-preview-action) ${{ steps.preview-step.outputs.action-version }}
|
|
58
|
+
:---:
|
|
59
|
+
Preview removed because the pull request was closed.
|
|
60
|
+
${{ steps.preview-step.outputs.action-start-time }}
|
package/README.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
# Component for lendable and embed items
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
 [](https://codecov.io/gh/internetarchive/iaux-book-actions)
|
|
3
|
+
|
|
4
|
+
# Internet Archive Book Lending Actions
|
|
5
|
+
|
|
6
|
+
This WebComponent, built using the [IA Typescript WebComponent Template](https://github.com/internetarchive/iaux-typescript-wc-template), provides support for Bookreader actions for borrowable books on Internet Archive. It extends the [Open WebComponents generator](https://open-wc.org/docs/development/generator/) with Internet Archive-specific features and developer tools.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
```bash
|
|
7
10
|
yarn install --save @internetarchive/ia-book-actions
|
|
8
11
|
```
|
|
9
12
|
|
|
13
|
+
## Local Demo with `web-dev-server`
|
|
14
|
+
```bash
|
|
15
|
+
yarn start
|
|
16
|
+
```
|
|
17
|
+
To run a local development server that serves the basic demo located in `index.html`
|
|
18
|
+
|
|
10
19
|
## Usage of Book Actions
|
|
11
20
|
```html
|
|
12
21
|
<!-- index.html -->
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@internetarchive/ia-book-actions",
|
|
3
|
-
"version": "1.4.1",
|
|
4
3
|
"description": "Bookreader actions for borrowable book",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/internetarchive/iaux-book-actions.git"
|
|
7
|
+
},
|
|
5
8
|
"author": "Internet Archive",
|
|
6
9
|
"license": "AGPL-3.0-only",
|
|
10
|
+
"version": "1.4.3-canary.0",
|
|
7
11
|
"main": "index.js",
|
|
8
12
|
"module": "index.js",
|
|
9
13
|
"publishConfig": {
|
|
@@ -15,28 +19,27 @@
|
|
|
15
19
|
"prepare": "rimraf dist && husky install",
|
|
16
20
|
"lint": "eslint --ext \"./src/*.js\",.html . --ignore-path .gitignore && prettier \"./src/*.js\" --check --ignore-path .gitignore",
|
|
17
21
|
"format": "eslint --ext \"./src/*.js\",.html . --fix --ignore-path .gitignore && prettier \"./src/*.js\" --write --ignore-path .gitignore",
|
|
18
|
-
"test": "yarn run lint && yarn test:fast",
|
|
19
|
-
"test:fast": "web-test-runner
|
|
22
|
+
"test": "yarn run format && yarn run lint && yarn test:fast --coverage",
|
|
23
|
+
"test:fast": "web-test-runner",
|
|
20
24
|
"test:watch": "web-test-runner --watch --playwright --browsers chromium firefox webkit",
|
|
21
25
|
"deploy": "yarn run deploy:run -e $(git branch --show-current)",
|
|
22
26
|
"deploy:run": "yarn run prepare:ghpages && touch ghpages/.nojekyll && yarn run deploy:gh",
|
|
23
27
|
"deploy:gh": "gh-pages -t -d ghpages -m \"Build for $(git log --pretty=format:\"%h %an %ai %s\" -n1) [skip ci]\""
|
|
24
28
|
},
|
|
25
29
|
"dependencies": {
|
|
26
|
-
"@internetarchive/icon-info": "1.
|
|
30
|
+
"@internetarchive/icon-info": "^1.4.1",
|
|
27
31
|
"@internetarchive/local-cache": "^0.2.1",
|
|
28
|
-
"@internetarchive/modal-manager": "2.0.0",
|
|
32
|
+
"@internetarchive/modal-manager": "^2.0.0",
|
|
29
33
|
"@internetarchive/shared-resize-observer": "^0.2.0",
|
|
30
|
-
"
|
|
31
|
-
"lit": "^2.8.0"
|
|
34
|
+
"lit": "^2.8.0 || ^3.3.2"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@open-wc/eslint-config": "^4.2.0",
|
|
35
|
-
"@open-wc/testing": "^
|
|
36
|
-
"@web/dev-server": "^0.
|
|
37
|
-
"@web/dev-server-esbuild": "^0.
|
|
38
|
-
"@web/test-runner": "^0.
|
|
39
|
-
"@web/test-runner-playwright": "^0.
|
|
38
|
+
"@open-wc/testing": "^4.0.0",
|
|
39
|
+
"@web/dev-server": "^0.4.6",
|
|
40
|
+
"@web/dev-server-esbuild": "^1.0.5",
|
|
41
|
+
"@web/test-runner": "^0.20.2",
|
|
42
|
+
"@web/test-runner-playwright": "^0.11.1",
|
|
40
43
|
"eslint": "^7.32.0",
|
|
41
44
|
"eslint-config-prettier": "^6.11.0",
|
|
42
45
|
"eslint-plugin-import": "^2.22.1",
|
package/src/ia-book-actions.js
CHANGED
|
@@ -98,6 +98,10 @@ export default class IABookActions extends LitElement {
|
|
|
98
98
|
*/
|
|
99
99
|
this.returnNow = false;
|
|
100
100
|
|
|
101
|
+
this.loanRenewInProgress = false;
|
|
102
|
+
|
|
103
|
+
this.warningModalOpen = false;
|
|
104
|
+
|
|
101
105
|
/**
|
|
102
106
|
* contains one hour auto-loan-renew time configuration
|
|
103
107
|
* defaults to 1 hour config
|
|
@@ -120,7 +124,12 @@ export default class IABookActions extends LitElement {
|
|
|
120
124
|
* @property {boolean} renewNow - key to determine if need to renew now
|
|
121
125
|
* @property {number} secondsLeft - seconds left in active loan
|
|
122
126
|
*/
|
|
123
|
-
this.loanRenewResult = {
|
|
127
|
+
this.loanRenewResult = {
|
|
128
|
+
texts: '',
|
|
129
|
+
renewNow: false,
|
|
130
|
+
secondsLeft: 0,
|
|
131
|
+
renewType: '',
|
|
132
|
+
};
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
disconnectedCallback() {
|
|
@@ -136,7 +145,6 @@ export default class IABookActions extends LitElement {
|
|
|
136
145
|
* @param {string} msg
|
|
137
146
|
*/
|
|
138
147
|
sentryCaptureMsg(msg) {
|
|
139
|
-
log(window?.Sentry);
|
|
140
148
|
window?.Sentry?.captureMessage(msg);
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -228,7 +236,7 @@ export default class IABookActions extends LitElement {
|
|
|
228
236
|
'browsingExpired' in this.lendingStatus &&
|
|
229
237
|
this.lendingStatus?.browsingExpired;
|
|
230
238
|
if (hasExpired) {
|
|
231
|
-
log('
|
|
239
|
+
log('[IABookActions] browsing expired — showing expired state');
|
|
232
240
|
|
|
233
241
|
if (!this.tokenPoller) {
|
|
234
242
|
this.sentryCaptureMsg(sentryLogs.bookWasExpired);
|
|
@@ -285,7 +293,11 @@ export default class IABookActions extends LitElement {
|
|
|
285
293
|
* dispatched this event from bookreader page changed
|
|
286
294
|
*/
|
|
287
295
|
window.addEventListener('BookReader:userAction', () => {
|
|
288
|
-
|
|
296
|
+
// If the loan expired while the tab stayed visible, auto-renew on page turn
|
|
297
|
+
if (this.lendingStatus.browsingExpired) {
|
|
298
|
+
this.autoRenewExpiredLoan();
|
|
299
|
+
}
|
|
300
|
+
|
|
289
301
|
if (this.borrowType === 'browsed') {
|
|
290
302
|
this.autoLoanRenewChecker(true);
|
|
291
303
|
}
|
|
@@ -298,17 +310,17 @@ export default class IABookActions extends LitElement {
|
|
|
298
310
|
*/
|
|
299
311
|
document.addEventListener('visibilitychange', async () => {
|
|
300
312
|
if (!document.hidden) {
|
|
301
|
-
log(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
this.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
) {
|
|
313
|
+
log('[IABookActions] visibilitychange:', this.borrowType);
|
|
314
|
+
|
|
315
|
+
// Loan already expired while tab was hidden — try to silently renew
|
|
316
|
+
if (this.lendingStatus.browsingExpired === true) {
|
|
317
|
+
this.autoRenewExpiredLoan();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.borrowType !== 'browsed') return;
|
|
322
|
+
|
|
323
|
+
if (this.lendingStatus.browsingExpired === false) {
|
|
312
324
|
const loanTime = await this.localCache.get(
|
|
313
325
|
`${this.identifier}-loanTime`
|
|
314
326
|
);
|
|
@@ -319,7 +331,8 @@ export default class IABookActions extends LitElement {
|
|
|
319
331
|
if (secondsLeft >= this.timerExecutionSeconds) {
|
|
320
332
|
this.loanStatusCheckInterval(Number(secondsLeft));
|
|
321
333
|
} else {
|
|
322
|
-
|
|
334
|
+
// Loan expired while user was away — try to silently renew
|
|
335
|
+
this.autoRenewExpiredLoan();
|
|
323
336
|
this.disconnectedCallback();
|
|
324
337
|
}
|
|
325
338
|
}
|
|
@@ -345,6 +358,36 @@ export default class IABookActions extends LitElement {
|
|
|
345
358
|
this.loanRenewResult = this.loanRenewHelper.result;
|
|
346
359
|
}
|
|
347
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Attempt to automatically renew a browse loan that has expired.
|
|
363
|
+
* Called on visibilitychange (user returns to tab) or on page turn when
|
|
364
|
+
* the loan has expired but the book may still be available.
|
|
365
|
+
*
|
|
366
|
+
* Reuses the same renew_loan path as the "Keep reading" warning modal button.
|
|
367
|
+
* On success: handleLoanAutoRenewed() resets the timer seamlessly.
|
|
368
|
+
* On error: handleLendingActionError() shows showLoanUnavailableModal().
|
|
369
|
+
*/
|
|
370
|
+
autoRenewExpiredLoan() {
|
|
371
|
+
if (this.loanRenewInProgress) return;
|
|
372
|
+
this.loanRenewInProgress = true;
|
|
373
|
+
|
|
374
|
+
this.modal?.closeModal();
|
|
375
|
+
|
|
376
|
+
// Optimistically flip browsingExpired back to false so the action bar
|
|
377
|
+
// stays red (patronIsReadingAction) while the renew_loan request is in
|
|
378
|
+
// flight, instead of showing the blue "Borrow" state.
|
|
379
|
+
this.lendingStatus = {
|
|
380
|
+
...this.lendingStatus,
|
|
381
|
+
browsingExpired: false,
|
|
382
|
+
secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Defer renewNow until after lendingStatus timer setup completes.
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
this.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
388
|
+
}, 0);
|
|
389
|
+
}
|
|
390
|
+
|
|
348
391
|
/**
|
|
349
392
|
* Required as sibling on page
|
|
350
393
|
* @returns HTMLElement
|
|
@@ -360,29 +403,34 @@ export default class IABookActions extends LitElement {
|
|
|
360
403
|
* Show the waring modal to ask user if they are still reading
|
|
361
404
|
*/
|
|
362
405
|
async showWarningModal() {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.modal.customModalContent = nothing;
|
|
366
|
-
this.modal?.closeModal();
|
|
367
|
-
this.loanRenewResult = { texts: '', renewNow: false };
|
|
406
|
+
if (this.warningModalOpen) return;
|
|
407
|
+
this.warningModalOpen = true;
|
|
368
408
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
409
|
+
log('[IABookActions] showWarningModal');
|
|
410
|
+
|
|
411
|
+
// Capture texts and secondsLeft before resetting loanRenewResult
|
|
412
|
+
const {
|
|
413
|
+
texts: warningTexts,
|
|
414
|
+
secondsLeft: rawSecondsLeft,
|
|
415
|
+
} = this.loanRenewResult;
|
|
416
|
+
let secondsLeft = rawSecondsLeft;
|
|
417
|
+
if (secondsLeft === undefined || secondsLeft <= 0) {
|
|
372
418
|
secondsLeft = this.lendingStatus.secondsLeftOnLoan;
|
|
373
419
|
} else {
|
|
374
420
|
secondsLeft = secondsLeft > 60 ? secondsLeft : 60;
|
|
375
421
|
}
|
|
376
422
|
|
|
423
|
+
// clear modal and reset renew state
|
|
424
|
+
this.modal.customModalContent = nothing;
|
|
425
|
+
this.modal?.closeModal();
|
|
426
|
+
this.loanRenewResult = { texts: '', renewNow: false };
|
|
427
|
+
|
|
377
428
|
const config = new ModalConfig({
|
|
378
429
|
headline: 'Are you still reading?',
|
|
379
430
|
headerColor: '#194880',
|
|
380
431
|
showCloseButton: false,
|
|
381
432
|
closeOnBackdropClick: false,
|
|
382
|
-
message: this.loanRenewHelper?.getMessageTexts(
|
|
383
|
-
this.loanRenewResult.texts,
|
|
384
|
-
secondsLeft
|
|
385
|
-
),
|
|
433
|
+
message: this.loanRenewHelper?.getMessageTexts(warningTexts, secondsLeft),
|
|
386
434
|
});
|
|
387
435
|
|
|
388
436
|
const customModalContent = html`<br />
|
|
@@ -479,6 +527,7 @@ export default class IABookActions extends LitElement {
|
|
|
479
527
|
|
|
480
528
|
/**
|
|
481
529
|
* Show modal when book is auto returned
|
|
530
|
+
* @deprecated
|
|
482
531
|
*/
|
|
483
532
|
async showExpiredModal() {
|
|
484
533
|
const config = new ModalConfig({
|
|
@@ -508,7 +557,7 @@ export default class IABookActions extends LitElement {
|
|
|
508
557
|
* Execute when loan is expired
|
|
509
558
|
*/
|
|
510
559
|
async browseHasExpired() {
|
|
511
|
-
log('
|
|
560
|
+
log('[IABookActions] browseHasExpired');
|
|
512
561
|
window?.IALendingIntervals?.clearAll();
|
|
513
562
|
|
|
514
563
|
const currStatus = {
|
|
@@ -527,11 +576,38 @@ export default class IABookActions extends LitElement {
|
|
|
527
576
|
this.loanRenewResult.texts =
|
|
528
577
|
'This book has been returned due to inactivity.';
|
|
529
578
|
|
|
530
|
-
|
|
579
|
+
this.modal?.closeModal();
|
|
531
580
|
|
|
532
581
|
this.sentryCaptureMsg(sentryLogs.browseHasExpired);
|
|
533
582
|
}
|
|
534
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Show modal when the book can no longer be automatically renewed
|
|
586
|
+
* because another patron has checked it out.
|
|
587
|
+
*/
|
|
588
|
+
async showLoanUnavailableModal() {
|
|
589
|
+
const config = new ModalConfig({
|
|
590
|
+
headline: '',
|
|
591
|
+
showCloseButton: false,
|
|
592
|
+
closeOnBackdropClick: false,
|
|
593
|
+
headerColor: '#194880',
|
|
594
|
+
message:
|
|
595
|
+
'Another patron is borrowing this book. Please check back later.',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const customModalContent = html`<br />
|
|
599
|
+
<div style="text-align: center">
|
|
600
|
+
<button
|
|
601
|
+
style="${modalButtonStyle.iaButton} ${modalButtonStyle.renew}"
|
|
602
|
+
@click=${() => URLHelper.goToUrl(this.returnUrl, true)}
|
|
603
|
+
>
|
|
604
|
+
Okay
|
|
605
|
+
</button>
|
|
606
|
+
</div>`;
|
|
607
|
+
|
|
608
|
+
await this.modal?.showModal({ config, customModalContent });
|
|
609
|
+
}
|
|
610
|
+
|
|
535
611
|
async startBrowseTimer() {
|
|
536
612
|
window?.IALendingIntervals?.clearBrowseExpireTimeout();
|
|
537
613
|
|
|
@@ -542,19 +618,10 @@ export default class IABookActions extends LitElement {
|
|
|
542
618
|
} = this.lendingStatus;
|
|
543
619
|
|
|
544
620
|
if (!user_has_browsed || browsingExpired) {
|
|
545
|
-
log('startBrowseTimer --- !user_has_browsed || browsingExpired', {
|
|
546
|
-
user_has_browsed,
|
|
547
|
-
browsingExpired,
|
|
548
|
-
secondsLeftOnLoan,
|
|
549
|
-
});
|
|
550
621
|
return;
|
|
551
622
|
}
|
|
552
623
|
|
|
553
624
|
window.IALendingIntervals.browseExpireTimeout = setTimeout(() => {
|
|
554
|
-
log(
|
|
555
|
-
'startBrowseTimer > browseExpireTimeout --- will expire loan',
|
|
556
|
-
secondsLeftOnLoan
|
|
557
|
-
);
|
|
558
625
|
this.browseHasExpired();
|
|
559
626
|
}, secondsLeftOnLoan * 1000);
|
|
560
627
|
}
|
|
@@ -636,12 +703,9 @@ export default class IABookActions extends LitElement {
|
|
|
636
703
|
|
|
637
704
|
// number of seconds left in current loan
|
|
638
705
|
const secondsLeft = Math.round((loanTime - new Date()) / 1000);
|
|
639
|
-
log(
|
|
640
|
-
|
|
641
|
-
log('IABookActions: handleLoanAutoRenewed --- ', {
|
|
706
|
+
log('[IABookActions] handleLoanAutoRenewed', {
|
|
707
|
+
secondsLeft,
|
|
642
708
|
ajaxResponse: detail?.data,
|
|
643
|
-
loanRenewResult: this.loanRenewResult,
|
|
644
|
-
secondsLeftOnLoan: secondsLeft,
|
|
645
709
|
});
|
|
646
710
|
|
|
647
711
|
const currStatus = {
|
|
@@ -658,6 +722,9 @@ export default class IABookActions extends LitElement {
|
|
|
658
722
|
this.modal.customModalContent = nothing;
|
|
659
723
|
this.sentryCaptureMsg(sentryLogs.bookHasRenewed);
|
|
660
724
|
}
|
|
725
|
+
|
|
726
|
+
this.warningModalOpen = false;
|
|
727
|
+
this.loanRenewInProgress = false;
|
|
661
728
|
}
|
|
662
729
|
|
|
663
730
|
/**
|
|
@@ -693,29 +760,19 @@ export default class IABookActions extends LitElement {
|
|
|
693
760
|
// re-sync timer if gone off because of background window
|
|
694
761
|
// side effect: updates this.lendingStatus & kicks off lifecycle,
|
|
695
762
|
// if really updated, escape from here
|
|
696
|
-
const resyncd =
|
|
763
|
+
const resyncd = this.reSyncTimerIfGoneOff(secondsLeft);
|
|
697
764
|
|
|
698
765
|
if (resyncd.hasSynced) {
|
|
699
766
|
secondsLeft = resyncd.whatShouldLeft;
|
|
700
|
-
log('startTimerCountdown
|
|
767
|
+
log('[IABookActions] startTimerCountdown: timer re-synced', {
|
|
701
768
|
secondsLeft,
|
|
702
769
|
});
|
|
703
770
|
}
|
|
704
771
|
|
|
705
|
-
log(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
secondsLeft,
|
|
710
|
-
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
711
|
-
},
|
|
712
|
-
'time: ',
|
|
713
|
-
new Date().getMinutes(),
|
|
714
|
-
':',
|
|
715
|
-
new Date().getSeconds(),
|
|
716
|
-
', timerDelay: ',
|
|
717
|
-
this.timerExecutionSeconds
|
|
718
|
-
);
|
|
772
|
+
log('[IABookActions] startTimerCountdown', {
|
|
773
|
+
secondsLeft,
|
|
774
|
+
loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
|
|
775
|
+
});
|
|
719
776
|
|
|
720
777
|
/**
|
|
721
778
|
* execute from last 10th minutes to 0th minute
|
|
@@ -740,7 +797,7 @@ export default class IABookActions extends LitElement {
|
|
|
740
797
|
* @param {number} timerSecondsLeft - actual seconds left get from setInterval
|
|
741
798
|
* @returns {Object} { hasSynced: [boolean], whatShouldLeft: [number] }
|
|
742
799
|
*/
|
|
743
|
-
|
|
800
|
+
reSyncTimerIfGoneOff(timerSecondsLeft) {
|
|
744
801
|
const currentTime = new Date();
|
|
745
802
|
|
|
746
803
|
// current time - loan time
|
|
@@ -750,27 +807,11 @@ export default class IABookActions extends LitElement {
|
|
|
750
807
|
const secondsShouldLeft =
|
|
751
808
|
this.lendingStatus.secondsLeftOnLoan - diffInSeconds;
|
|
752
809
|
|
|
753
|
-
log('currentTime: ', currentTime);
|
|
754
|
-
log('timeWhenTimerStart: ', this.timeWhenTimerStart);
|
|
755
|
-
log('diffInSeconds: ', diffInSeconds);
|
|
756
|
-
log('secondsShouldLeft: ', secondsShouldLeft);
|
|
757
|
-
log(
|
|
758
|
-
'this.lendingStatus.secondsLeftOnLoan: ',
|
|
759
|
-
this.lendingStatus.secondsLeftOnLoan
|
|
760
|
-
);
|
|
761
|
-
|
|
762
810
|
// convert in minutes
|
|
763
811
|
const whatIsleft = Math.round(timerSecondsLeft);
|
|
764
812
|
const whatShouldLeft = Math.round(secondsShouldLeft);
|
|
765
813
|
const timerElSeconds = this.timerCountdownEl.secondsLeftOnLoan || 0;
|
|
766
814
|
|
|
767
|
-
log(`reSyncTimerIfGoneOff?`, {
|
|
768
|
-
whatIsleft,
|
|
769
|
-
whatShouldLeft,
|
|
770
|
-
timerElSeconds,
|
|
771
|
-
timeLeftInMin: Math.ceil(timerSecondsLeft / 60),
|
|
772
|
-
});
|
|
773
|
-
|
|
774
815
|
if (timerElSeconds !== whatShouldLeft || whatIsleft !== whatShouldLeft) {
|
|
775
816
|
// set lending status with new time to update decrementor
|
|
776
817
|
const currStatus = {
|
|
@@ -781,7 +822,9 @@ export default class IABookActions extends LitElement {
|
|
|
781
822
|
}
|
|
782
823
|
|
|
783
824
|
if (whatIsleft !== whatShouldLeft) {
|
|
784
|
-
log(
|
|
825
|
+
log(
|
|
826
|
+
`[IABookActions] reSyncTimerIfGoneOff ${whatIsleft} - ${whatShouldLeft}: re-syncing timer`
|
|
827
|
+
);
|
|
785
828
|
return { hasSynced: true, whatShouldLeft };
|
|
786
829
|
}
|
|
787
830
|
|
|
@@ -797,10 +840,6 @@ export default class IABookActions extends LitElement {
|
|
|
797
840
|
* @memberof IABookActions
|
|
798
841
|
*/
|
|
799
842
|
async loanRenewAttempt(secondsLeft) {
|
|
800
|
-
log('loanRenewAttempt ---', {
|
|
801
|
-
secondsLeft,
|
|
802
|
-
loanRenewResult: this.loanRenewResult,
|
|
803
|
-
});
|
|
804
843
|
let loanSecondsLeft = secondsLeft;
|
|
805
844
|
/**
|
|
806
845
|
* auto-renew is not possible in last seconds (let say 50 second) because,
|
|
@@ -810,7 +849,7 @@ export default class IABookActions extends LitElement {
|
|
|
810
849
|
* so if seconds left is < 50, just expire the loan
|
|
811
850
|
*/
|
|
812
851
|
if (loanSecondsLeft < 50) {
|
|
813
|
-
log('loanRenewAttempt
|
|
852
|
+
log('[IABookActions] loanRenewAttempt: < 50s left, expiring loan');
|
|
814
853
|
await this.browseHasExpired();
|
|
815
854
|
return;
|
|
816
855
|
}
|
|
@@ -892,9 +931,17 @@ export default class IABookActions extends LitElement {
|
|
|
892
931
|
const action = event?.detail?.action;
|
|
893
932
|
const errorMsg = event?.detail?.data?.error;
|
|
894
933
|
|
|
895
|
-
//
|
|
896
|
-
if (
|
|
934
|
+
// For renew_loan failures after auto-renew, show the patron-facing message
|
|
935
|
+
if (
|
|
936
|
+
errorMsg &&
|
|
937
|
+
action === 'renew_loan' &&
|
|
938
|
+
errorMsg.match(/not available/)
|
|
939
|
+
) {
|
|
940
|
+
this.loanRenewInProgress = false;
|
|
941
|
+
this.showLoanUnavailableModal();
|
|
942
|
+
} else if (errorMsg && action !== 'create_token') {
|
|
897
943
|
this.showErrorModal(errorMsg, action);
|
|
944
|
+
}
|
|
898
945
|
|
|
899
946
|
// if error related to loan token
|
|
900
947
|
// - set user_has_browsed to `false`
|
|
@@ -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.
|
|
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', () => {
|
|
@@ -467,7 +488,7 @@ describe('Visibility change API for document', async () => {
|
|
|
467
488
|
});
|
|
468
489
|
});
|
|
469
490
|
|
|
470
|
-
it('when book is not expired', async () => {
|
|
491
|
+
it('when book is not expired and loan time is valid', async () => {
|
|
471
492
|
const el = await fixture(
|
|
472
493
|
container({
|
|
473
494
|
userid: '@user1',
|
|
@@ -480,31 +501,220 @@ describe('Visibility change API for document', async () => {
|
|
|
480
501
|
);
|
|
481
502
|
await el.updateComplete;
|
|
482
503
|
|
|
504
|
+
// Set a valid loan time well into the future
|
|
505
|
+
await el.localCache.set({
|
|
506
|
+
key: 'foobar-loanTime',
|
|
507
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
508
|
+
ttl: 3600,
|
|
509
|
+
});
|
|
510
|
+
|
|
483
511
|
const spy = Sinon.spy(el, 'loanStatusCheckInterval');
|
|
484
512
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
513
|
+
await aTimeout(100);
|
|
514
|
+
|
|
515
|
+
expect(spy.calledOnce).to.be.true;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('when loan expired while tab was hidden (browsingExpired false, loanTime past)', async () => {
|
|
519
|
+
const el = await fixture(
|
|
520
|
+
container({
|
|
521
|
+
userid: '@user1',
|
|
522
|
+
identifier: 'foobar',
|
|
523
|
+
lendingStatus: {
|
|
524
|
+
user_has_browsed: true,
|
|
525
|
+
browsingExpired: false,
|
|
526
|
+
},
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
await el.updateComplete;
|
|
530
|
+
|
|
531
|
+
// Set a loan time already in the past
|
|
532
|
+
await el.localCache.set({
|
|
533
|
+
key: 'foobar-loanTime',
|
|
534
|
+
value: new Date(new Date().getTime() - 10 * 1000),
|
|
535
|
+
ttl: 3600,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
539
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
540
|
+
await aTimeout(100);
|
|
485
541
|
|
|
486
542
|
expect(spy.calledOnce).to.be.true;
|
|
487
543
|
});
|
|
488
544
|
|
|
489
|
-
it('when
|
|
545
|
+
it('when loan already marked expired (browsingExpired true)', async () => {
|
|
490
546
|
const el = await fixture(
|
|
491
547
|
container({
|
|
492
548
|
userid: '@user1',
|
|
493
549
|
identifier: 'foobar',
|
|
494
550
|
lendingStatus: {
|
|
495
551
|
user_has_browsed: true,
|
|
552
|
+
browsingExpired: true,
|
|
496
553
|
},
|
|
497
554
|
})
|
|
498
555
|
);
|
|
499
556
|
await el.updateComplete;
|
|
500
557
|
|
|
501
|
-
|
|
558
|
+
el.borrowType = 'browsed';
|
|
559
|
+
|
|
560
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
502
561
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
562
|
+
await aTimeout(100);
|
|
503
563
|
|
|
504
564
|
expect(spy.calledOnce).to.be.true;
|
|
505
565
|
});
|
|
506
566
|
});
|
|
507
567
|
|
|
568
|
+
describe('autoRenewExpiredLoan', () => {
|
|
569
|
+
it('optimistically resets browsingExpired so the button stays red', async () => {
|
|
570
|
+
const el = await fixture(
|
|
571
|
+
container({
|
|
572
|
+
userid: '@user1',
|
|
573
|
+
identifier: 'foobar',
|
|
574
|
+
lendingStatus: {
|
|
575
|
+
user_has_browsed: true,
|
|
576
|
+
available_to_browse: true,
|
|
577
|
+
browsingExpired: true,
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
await el.updateComplete;
|
|
582
|
+
|
|
583
|
+
// Before: browsingExpired true → borrow1HrAction → primary color is 'primary' (blue)
|
|
584
|
+
expect(el.primaryColor).to.equal('primary');
|
|
585
|
+
|
|
586
|
+
el.autoRenewExpiredLoan();
|
|
587
|
+
await el.updateComplete;
|
|
588
|
+
|
|
589
|
+
// After: browsingExpired flipped to false → patronIsReadingAction → 'danger' (red)
|
|
590
|
+
expect(el.lendingStatus.browsingExpired).to.be.false;
|
|
591
|
+
expect(el.primaryColor).to.equal('danger');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
|
|
595
|
+
const el = await fixture(
|
|
596
|
+
container({
|
|
597
|
+
userid: '@user1',
|
|
598
|
+
identifier: 'foobar',
|
|
599
|
+
lendingStatus: {
|
|
600
|
+
user_has_browsed: true,
|
|
601
|
+
browsingExpired: true,
|
|
602
|
+
},
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
await el.updateComplete;
|
|
606
|
+
|
|
607
|
+
el.autoRenewExpiredLoan();
|
|
608
|
+
|
|
609
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
610
|
+
|
|
611
|
+
// renewNow is set in a setTimeout — flush the macrotask queue
|
|
612
|
+
await aTimeout(50);
|
|
613
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
614
|
+
expect(el.loanRenewResult.renewType).to.equal('auto');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('ignores subsequent calls while a renewal is already in progress', async () => {
|
|
618
|
+
const el = await fixture(
|
|
619
|
+
container({
|
|
620
|
+
userid: '@user1',
|
|
621
|
+
identifier: 'foobar',
|
|
622
|
+
lendingStatus: {
|
|
623
|
+
user_has_browsed: true,
|
|
624
|
+
browsingExpired: true,
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
await el.updateComplete;
|
|
629
|
+
|
|
630
|
+
// Spy *after* fixture so the handler is the same instance
|
|
631
|
+
const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
|
|
632
|
+
|
|
633
|
+
el.autoRenewExpiredLoan(); // first call — proceeds
|
|
634
|
+
el.autoRenewExpiredLoan(); // second call — should be a no-op
|
|
635
|
+
|
|
636
|
+
// The spy counts both invocations but the internal guard stops the second
|
|
637
|
+
expect(spy.callCount).to.equal(2);
|
|
638
|
+
// loanRenewInProgress is still true (not double-set or cleared)
|
|
639
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
640
|
+
|
|
641
|
+
// Only one setTimeout fires (the second call returned early)
|
|
642
|
+
await aTimeout(50);
|
|
643
|
+
expect(el.loanRenewResult.renewNow).to.be.true;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('clears loanRenewInProgress after a successful loanAutoRenewed event', async () => {
|
|
647
|
+
const el = await fixture(
|
|
648
|
+
container({
|
|
649
|
+
userid: '@user1',
|
|
650
|
+
identifier: 'foobar',
|
|
651
|
+
lendingStatus: {
|
|
652
|
+
user_has_browsed: true,
|
|
653
|
+
browsingExpired: false,
|
|
654
|
+
secondsLeftOnLoan: 100,
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
await el.updateComplete;
|
|
659
|
+
|
|
660
|
+
await el.localCache.set({
|
|
661
|
+
key: 'foobar-loanTime',
|
|
662
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
663
|
+
ttl: 3600,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
el.autoRenewExpiredLoan();
|
|
667
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
668
|
+
|
|
669
|
+
// Wait for the deferred renewNow to be set
|
|
670
|
+
await aTimeout(50);
|
|
671
|
+
|
|
672
|
+
// Simulate the renew_loan success response
|
|
673
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
674
|
+
'collapsible-action-group'
|
|
675
|
+
);
|
|
676
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
677
|
+
new CustomEvent('loanAutoRenewed', {
|
|
678
|
+
detail: { data: { loan: { identifier: 'foobar' } } },
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
await aTimeout(100);
|
|
683
|
+
await el.updateComplete;
|
|
684
|
+
|
|
685
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
689
|
+
const el = await fixture(
|
|
690
|
+
container({
|
|
691
|
+
userid: '@user1',
|
|
692
|
+
identifier: 'foobar',
|
|
693
|
+
lendingStatus: {
|
|
694
|
+
user_has_browsed: true,
|
|
695
|
+
browsingExpired: true,
|
|
696
|
+
},
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
await el.updateComplete;
|
|
700
|
+
|
|
701
|
+
const showModalSpy = Sinon.spy(el, 'showLoanUnavailableModal');
|
|
702
|
+
|
|
703
|
+
el.autoRenewExpiredLoan();
|
|
704
|
+
expect(el.loanRenewInProgress).to.be.true;
|
|
705
|
+
|
|
706
|
+
el.handleLendingActionError({
|
|
707
|
+
detail: {
|
|
708
|
+
action: 'renew_loan',
|
|
709
|
+
data: { error: 'book is not available' },
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
expect(el.loanRenewInProgress).to.be.false;
|
|
714
|
+
expect(showModalSpy.calledOnce).to.be.true;
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
508
718
|
describe('Shared Resize Observer', () => {
|
|
509
719
|
it('can receive a Shared Resize Observer', async () => {
|
|
510
720
|
const sharedObserverStub = new SharedResizeObserver();
|