test(frontend): unit-test files and github actions
This commit is contained in:
parent
23c3db0613
commit
a9e12024c5
12 changed files with 888 additions and 3 deletions
44
.github/workflows/test.yml
vendored
Normal file
44
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Run FE unit tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
frontend-tests:
|
||||
name: Frontend Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install pnpm and run install
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.10.0
|
||||
working-directory: ui
|
||||
run_install: true
|
||||
|
||||
- name: Generate coverage report
|
||||
working-directory: ui
|
||||
run: pnpm test:coverage
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v4
|
||||
if: success()
|
||||
with:
|
||||
directory: ui/coverage
|
||||
flags: frontend
|
||||
fail_ci_if_error: false
|
||||
continue-on-error: true
|
||||
|
|
@ -83,7 +83,10 @@
|
|||
"defaultConfiguration": "development"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"coverageExclude": ["**/index.ts", "src/app/interfaces/*.ts", "src/app/theme.ts"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"build": "ng build",
|
||||
"build:watch": "ng build --watch",
|
||||
"test": "ng test",
|
||||
"test:coverage": "ng test --coverage",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"prettier": {
|
||||
|
|
@ -52,6 +53,9 @@
|
|||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@angular/localize": "^21.0.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@testing-library/angular": "^18.1.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"angular-eslint": "21.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"jsdom": "^27.1.0",
|
||||
|
|
|
|||
|
|
@ -90,6 +90,15 @@ importers:
|
|||
'@eslint/js':
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.2
|
||||
'@testing-library/angular':
|
||||
specifier: ^18.1.1
|
||||
version: 18.1.1(d594df7ad7bf2a0a81d88b699d7cab95)
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.6.1
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.15
|
||||
version: 4.0.15(vitest@4.0.15(@types/node@25.0.1)(jsdom@27.3.0(postcss@8.5.6))(sass@1.93.2))
|
||||
angular-eslint:
|
||||
specifier: 21.1.0
|
||||
version: 21.1.0(@angular/cli@21.0.3(@types/node@25.0.1)(chokidar@4.0.3))(chokidar@4.0.3)(eslint@9.39.2)(typescript-eslint@8.47.0(eslint@9.39.2)(typescript@5.9.3))(typescript@5.9.3)
|
||||
|
|
@ -365,6 +374,15 @@ packages:
|
|||
'@angular/animations':
|
||||
optional: true
|
||||
|
||||
'@angular/router@21.0.5':
|
||||
resolution: {integrity: sha512-IFmf0Wd7jSOoZ8TI+4RXMsYmnIfHQG+kGxeMQVKrefTdr3uEHW/TEsNzbW5bkCpVJHRm4EhkH4hSu8D8tUQffQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
'@angular/common': 21.0.5
|
||||
'@angular/core': 21.0.5
|
||||
'@angular/platform-browser': 21.0.5
|
||||
rxjs: ^6.5.3 || ^7.4.0
|
||||
|
||||
'@angular/service-worker@21.0.5':
|
||||
resolution: {integrity: sha512-Jh7e4XOpQUWsLL3IB1He/2ZPTyULZwRdfneU5rrG2PjGlGus2PrLrDQ2lVlEZyQwAGPk1eyabcDoZKXIJ0uVig==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
|
@ -445,6 +463,10 @@ packages:
|
|||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -457,6 +479,10 @@ packages:
|
|||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1646,6 +1672,25 @@ packages:
|
|||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
'@testing-library/angular@18.1.1':
|
||||
resolution: {integrity: sha512-LbA+W+VeOf7TC7/ZfHLiOLlLyD2cVG3mBdkJapviC2Fd4Bw/Utcaso4bh+5B0cx/fyKyuPgS+L6FnaKGdP9HBA==}
|
||||
peerDependencies:
|
||||
'@angular/common': '>= 20.0.0'
|
||||
'@angular/core': '>= 20.0.0'
|
||||
'@angular/platform-browser': '>= 20.0.0'
|
||||
'@angular/router': '>= 20.0.0'
|
||||
'@testing-library/dom': ^10.0.0
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@testing-library/user-event@14.6.1':
|
||||
resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': '>=7.21.4'
|
||||
|
||||
'@tufjs/canonical-json@2.0.0':
|
||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||
engines: {node: ^16.14.0 || >=18.0.0}
|
||||
|
|
@ -1657,6 +1702,9 @@ packages:
|
|||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/aria-query@5.0.4':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
|
|
@ -1789,6 +1837,15 @@ packages:
|
|||
peerDependencies:
|
||||
vite: ^6.0.0 || ^7.0.0
|
||||
|
||||
'@vitest/coverage-v8@4.0.15':
|
||||
resolution: {integrity: sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 4.0.15
|
||||
vitest: 4.0.15
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@4.0.15':
|
||||
resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==}
|
||||
|
||||
|
|
@ -1889,6 +1946,10 @@ packages:
|
|||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -1896,6 +1957,9 @@ packages:
|
|||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
aria-query@5.3.2:
|
||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -1904,6 +1968,9 @@ packages:
|
|||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-v8-to-istanbul@0.3.8:
|
||||
resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2115,6 +2182,10 @@ packages:
|
|||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
|
@ -2124,6 +2195,9 @@ packages:
|
|||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
|
|
@ -2447,6 +2521,9 @@ packages:
|
|||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
htmlparser2@10.0.0:
|
||||
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
|
||||
|
||||
|
|
@ -2568,12 +2645,27 @@ packages:
|
|||
resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jose@6.1.3:
|
||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-tokens@9.0.1:
|
||||
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
|
@ -2657,12 +2749,23 @@ packages:
|
|||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.19:
|
||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
magicast@0.5.1:
|
||||
resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==}
|
||||
|
||||
make-dir@4.0.0:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
make-fetch-happen@15.0.3:
|
||||
resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
|
@ -2976,6 +3079,10 @@ packages:
|
|||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
proc-log@5.0.0:
|
||||
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
|
@ -3011,6 +3118,9 @@ packages:
|
|||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
|
@ -3960,6 +4070,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@angular/animations': 21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))
|
||||
|
||||
'@angular/router@21.0.5(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.0.5(@angular/animations@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)':
|
||||
dependencies:
|
||||
'@angular/common': 21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
|
||||
'@angular/core': 21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)
|
||||
'@angular/platform-browser': 21.0.5(@angular/animations@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))
|
||||
rxjs: 7.8.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@angular/service-worker@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)':
|
||||
dependencies:
|
||||
'@angular/core': 21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)
|
||||
|
|
@ -4069,6 +4187,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -4092,6 +4212,8 @@ snapshots:
|
|||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
|
|
@ -4990,6 +5112,30 @@ snapshots:
|
|||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@testing-library/angular@18.1.1(d594df7ad7bf2a0a81d88b699d7cab95)':
|
||||
dependencies:
|
||||
'@angular/common': 21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)
|
||||
'@angular/core': 21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)
|
||||
'@angular/platform-browser': 21.0.5(@angular/animations@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))
|
||||
'@angular/router': 21.0.5(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.0.5(@angular/animations@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.0.5(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.0.5(@angular/compiler@21.0.5)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)
|
||||
'@testing-library/dom': 10.4.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
dom-accessibility-api: 0.5.16
|
||||
lz-string: 1.5.0
|
||||
picocolors: 1.1.1
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
|
||||
'@tufjs/canonical-json@2.0.0': {}
|
||||
|
||||
'@tufjs/models@4.0.0':
|
||||
|
|
@ -5002,6 +5148,8 @@ snapshots:
|
|||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
|
|
@ -5190,6 +5338,23 @@ snapshots:
|
|||
dependencies:
|
||||
vite: 7.2.2(@types/node@25.0.1)(sass@1.93.2)
|
||||
|
||||
'@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@25.0.1)(jsdom@27.3.0(postcss@8.5.6))(sass@1.93.2))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.0.15
|
||||
ast-v8-to-istanbul: 0.3.8
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6
|
||||
istanbul-reports: 3.2.0
|
||||
magicast: 0.5.1
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.15(@types/node@25.0.1)(jsdom@27.3.0(postcss@8.5.6))(sass@1.93.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@4.0.15':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
|
|
@ -5317,14 +5482,26 @@ snapshots:
|
|||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-v8-to-istanbul@0.3.8:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
estree-walker: 3.0.3
|
||||
js-tokens: 9.0.1
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
|
@ -5534,12 +5711,16 @@ snapshots:
|
|||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
optional: true
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
|
@ -5951,6 +6132,8 @@ snapshots:
|
|||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
htmlparser2@10.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
|
@ -6059,10 +6242,31 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
dependencies:
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
make-dir: 4.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
debug: 4.4.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-reports@3.2.0:
|
||||
dependencies:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
jose@6.1.3: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
|
@ -6173,6 +6377,8 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
magic-string@0.30.19:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
@ -6181,6 +6387,16 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.5.1:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
source-map-js: 1.2.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
||||
make-fetch-happen@15.0.3:
|
||||
dependencies:
|
||||
'@npmcli/agent': 4.0.0
|
||||
|
|
@ -6535,6 +6751,12 @@ snapshots:
|
|||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
proc-log@5.0.0: {}
|
||||
|
||||
proc-log@6.1.0: {}
|
||||
|
|
@ -6566,6 +6788,8 @@ snapshots:
|
|||
iconv-lite: 0.7.1
|
||||
unpipe: 1.0.0
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
|
|
|||
34
ui/src/app/components/master-checkbox.component.spec.ts
Normal file
34
ui/src/app/components/master-checkbox.component.spec.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* eslint-disable @angular-eslint/use-component-selector */
|
||||
import { render, screen } from '@testing-library/angular';
|
||||
import { MasterCheckboxComponent } from './master-checkbox.component';
|
||||
import { Checkable } from '../interfaces';
|
||||
|
||||
describe('MasterCheckboxComponent', () => {
|
||||
async function createMasterCheckboxComponent(
|
||||
override: { template?: string; componentProperties?: Record<string, unknown> } = {},
|
||||
) {
|
||||
override.template ||= `
|
||||
<app-master-checkbox [id]="'done'" [list]="doneList" (changed)="doneSelectionChanged($event)" />
|
||||
`;
|
||||
|
||||
const rendered = await render(override.template, {
|
||||
imports: [MasterCheckboxComponent],
|
||||
componentProperties: {
|
||||
doneList: new Map<string, Checkable>([
|
||||
['url1', { checked: false }],
|
||||
['url2', { checked: false }],
|
||||
]),
|
||||
doneSelectionChanged: (event: Event) => { console.log(event)},
|
||||
...override.componentProperties,
|
||||
},
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
it('should create the component', async () => {
|
||||
const { fixture } = await createMasterCheckboxComponent();
|
||||
expect(fixture.componentInstance).toBeTruthy();
|
||||
expect(screen.getByRole('checkbox')).toBeDefined();
|
||||
});
|
||||
});
|
||||
80
ui/src/app/components/slave-checkbox.component.spec.ts
Normal file
80
ui/src/app/components/slave-checkbox.component.spec.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* eslint-disable @angular-eslint/use-component-selector */
|
||||
import { render, screen } from '@testing-library/angular';
|
||||
import { SlaveCheckboxComponent } from './slave-checkbox.component';
|
||||
import { MasterCheckboxComponent } from './master-checkbox.component';
|
||||
|
||||
describe('SlaveCheckboxComponent', () => {
|
||||
async function createSlaveCheckboxComponent(
|
||||
override: { template?: string; componentProperties?: Record<string, unknown> } = {},
|
||||
) {
|
||||
override.template ||= `
|
||||
<app-slave-checkbox
|
||||
[id]="'item1'"
|
||||
[master]="masterCheckbox"
|
||||
[checkable]="checkable"
|
||||
/>
|
||||
`;
|
||||
|
||||
const mockMasterCheckbox = {
|
||||
id: () => 'master',
|
||||
selectionChanged: vi.fn(),
|
||||
} as unknown as MasterCheckboxComponent;
|
||||
|
||||
const rendered = await render(override.template, {
|
||||
imports: [SlaveCheckboxComponent],
|
||||
componentProperties: {
|
||||
masterCheckbox: mockMasterCheckbox,
|
||||
checkable: { checked: false },
|
||||
...override.componentProperties,
|
||||
},
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
it('should create the component', async () => {
|
||||
const { fixture } = await createSlaveCheckboxComponent();
|
||||
expect(fixture.componentInstance).toBeTruthy();
|
||||
expect(screen.getByRole('checkbox')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render unchecked checkbox by default', async () => {
|
||||
await createSlaveCheckboxComponent();
|
||||
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should render checked checkbox when checkable.checked is true', async () => {
|
||||
await createSlaveCheckboxComponent({
|
||||
componentProperties: {
|
||||
checkable: { checked: true },
|
||||
},
|
||||
});
|
||||
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
it('should have correct id attribute', async () => {
|
||||
const mockMasterCheckbox = {
|
||||
id: () => 'queue',
|
||||
selectionChanged: vi.fn(),
|
||||
} as unknown as MasterCheckboxComponent;
|
||||
|
||||
await createSlaveCheckboxComponent({
|
||||
componentProperties: {
|
||||
masterCheckbox: mockMasterCheckbox,
|
||||
checkable: { checked: false },
|
||||
},
|
||||
template: `
|
||||
<app-slave-checkbox
|
||||
[id]="'url123'"
|
||||
[master]="masterCheckbox"
|
||||
[checkable]="checkable"
|
||||
/>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
|
||||
expect(checkbox.id).toBe('queue-url123-select');
|
||||
});
|
||||
});
|
||||
52
ui/src/app/pipes/eta.pipe.spec.ts
Normal file
52
ui/src/app/pipes/eta.pipe.spec.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { EtaPipe } from './eta.pipe';
|
||||
|
||||
describe('EtaPipe', () => {
|
||||
let pipe: EtaPipe;
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new EtaPipe();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return null for null input', () => {
|
||||
expect(pipe.transform(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('should format seconds less than 60', () => {
|
||||
expect(pipe.transform(0)).toBe('0s');
|
||||
expect(pipe.transform(30)).toBe('30s');
|
||||
expect(pipe.transform(59)).toBe('59s');
|
||||
expect(pipe.transform(59.4)).toBe('59s');
|
||||
expect(pipe.transform(59.6)).toBe('60s');
|
||||
});
|
||||
|
||||
it('should format time between 60 seconds and 1 hour', () => {
|
||||
expect(pipe.transform(60)).toBe('1m 0s');
|
||||
expect(pipe.transform(90)).toBe('1m 30s');
|
||||
expect(pipe.transform(150)).toBe('2m 30s');
|
||||
expect(pipe.transform(3599)).toBe('59m 59s');
|
||||
});
|
||||
|
||||
it('should format time over 1 hour', () => {
|
||||
expect(pipe.transform(3600)).toBe('1h 0m 0s');
|
||||
expect(pipe.transform(3661)).toBe('1h 1m 1s');
|
||||
expect(pipe.transform(7200)).toBe('2h 0m 0s');
|
||||
expect(pipe.transform(7323)).toBe('2h 2m 3s');
|
||||
expect(pipe.transform(36000)).toBe('10h 0m 0s');
|
||||
});
|
||||
|
||||
it('should handle large time values', () => {
|
||||
expect(pipe.transform(86400)).toBe('24h 0m 0s'); // 1 day
|
||||
expect(pipe.transform(90061)).toBe('25h 1m 1s'); // 25 hours, 1 minute, 1 second
|
||||
});
|
||||
|
||||
it('should round seconds appropriately', () => {
|
||||
expect(pipe.transform(59.4)).toBe('59s');
|
||||
expect(pipe.transform(59.6)).toBe('60s');
|
||||
expect(pipe.transform(3600.4)).toBe('1h 0m 0s');
|
||||
expect(pipe.transform(3600.6)).toBe('1h 0m 1s');
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import { Pipe, PipeTransform } from "@angular/core";
|
|||
name: 'eta',
|
||||
})
|
||||
export class EtaPipe implements PipeTransform {
|
||||
transform(value: number): string | null {
|
||||
transform(value: number | null): string | null {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
57
ui/src/app/pipes/file-size.pipe.spec.ts
Normal file
57
ui/src/app/pipes/file-size.pipe.spec.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { FileSizePipe } from './file-size.pipe';
|
||||
|
||||
describe('FileSizePipe', () => {
|
||||
let pipe: FileSizePipe;
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new FileSizePipe();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return "0 Bytes" for zero or invalid input', () => {
|
||||
expect(pipe.transform(0)).toBe('0 Bytes');
|
||||
expect(pipe.transform(NaN)).toBe('0 Bytes');
|
||||
});
|
||||
|
||||
it('should format bytes correctly', () => {
|
||||
expect(pipe.transform(500)).toBe('500.00 Bytes');
|
||||
expect(pipe.transform(999)).toBe('999.00 Bytes');
|
||||
});
|
||||
|
||||
it('should format kilobytes correctly', () => {
|
||||
expect(pipe.transform(1000)).toBe('1.00 KB');
|
||||
expect(pipe.transform(1500)).toBe('1.50 KB');
|
||||
expect(pipe.transform(999999)).toBe('1000.00 KB');
|
||||
});
|
||||
|
||||
it('should format megabytes correctly', () => {
|
||||
expect(pipe.transform(1000000)).toBe('1.00 MB');
|
||||
expect(pipe.transform(1500000)).toBe('1.50 MB');
|
||||
expect(pipe.transform(52428800)).toBe('52.43 MB'); // 50 MB in binary, ~52.43 MB in decimal
|
||||
});
|
||||
|
||||
it('should format gigabytes correctly', () => {
|
||||
expect(pipe.transform(1000000000)).toBe('1.00 GB');
|
||||
expect(pipe.transform(1500000000)).toBe('1.50 GB');
|
||||
expect(pipe.transform(5368709120)).toBe('5.37 GB'); // 5 GB in binary, ~5.37 GB in decimal
|
||||
});
|
||||
|
||||
it('should format terabytes correctly', () => {
|
||||
expect(pipe.transform(1000000000000)).toBe('1.00 TB');
|
||||
expect(pipe.transform(1500000000000)).toBe('1.50 TB');
|
||||
});
|
||||
|
||||
it('should format very large sizes correctly', () => {
|
||||
expect(pipe.transform(1000000000000000)).toBe('1.00 PB'); // Petabyte
|
||||
expect(pipe.transform(1000000000000000000)).toBe('1.00 EB'); // Exabyte
|
||||
});
|
||||
|
||||
it('should always show 2 decimal places', () => {
|
||||
expect(pipe.transform(1234567)).toBe('1.23 MB');
|
||||
expect(pipe.transform(1234567890)).toBe('1.23 GB');
|
||||
expect(pipe.transform(1234)).toBe('1.23 KB');
|
||||
});
|
||||
});
|
||||
|
|
@ -28,7 +28,7 @@ export class SpeedPipe implements PipeTransform {
|
|||
});
|
||||
}
|
||||
|
||||
transform(value: number): string {
|
||||
transform(value: number | null | undefined): string {
|
||||
// If speed is invalid or 0, return empty string
|
||||
if (value === null || value === undefined || isNaN(value) || value <= 0) {
|
||||
return '';
|
||||
|
|
|
|||
306
ui/src/app/services/downloads.service.spec.ts
Normal file
306
ui/src/app/services/downloads.service.spec.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { DownloadsService } from './downloads.service';
|
||||
import { MeTubeSocket } from './metube-socket.service';
|
||||
import { Download, Status } from '../interfaces';
|
||||
import { of, Observable } from 'rxjs';
|
||||
|
||||
describe('DownloadsService', () => {
|
||||
let service: DownloadsService;
|
||||
let httpMock: HttpTestingController;
|
||||
let mockSocket: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock socket with proper typing
|
||||
const fromEventMock = <T = any>(event: string): Observable<T> => {
|
||||
// Return properly formatted data for different events
|
||||
if (event === 'all') {
|
||||
return of('[[],[]]' as any); // Empty queue and done arrays
|
||||
} else if (event === 'configuration') {
|
||||
return of('{}' as any); // Empty configuration object
|
||||
} else if (event === 'custom_dirs') {
|
||||
return of('{}' as any); // Empty custom dirs object
|
||||
}
|
||||
return of('[]' as any); // Default for other events
|
||||
};
|
||||
|
||||
mockSocket = {
|
||||
fromEvent: fromEventMock,
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
DownloadsService,
|
||||
{ provide: MeTubeSocket, useValue: mockSocket },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DownloadsService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have queue and done maps initialized', () => {
|
||||
// The service initializes with socket events, so we just verify the maps exist
|
||||
expect(service.queue).toBeInstanceOf(Map);
|
||||
expect(service.done).toBeInstanceOf(Map);
|
||||
expect(service.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should add a download via HTTP POST', () => {
|
||||
const mockStatus: Status = { status: 'success' };
|
||||
const url = 'https://example.com/video';
|
||||
const quality = 'best';
|
||||
const format = 'mp4';
|
||||
|
||||
service.add(url, quality, format, '', '', false, 0, true).subscribe((status) => {
|
||||
expect(status).toEqual(mockStatus);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne('add');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({
|
||||
url,
|
||||
quality,
|
||||
format,
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
auto_start: true,
|
||||
});
|
||||
req.flush(mockStatus);
|
||||
});
|
||||
|
||||
it('should start downloads by id', () => {
|
||||
const ids = ['url1', 'url2'];
|
||||
|
||||
service.startById(ids).subscribe();
|
||||
|
||||
const req = httpMock.expectOne('start');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ ids });
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('should delete downloads by id and mark them as deleting', () => {
|
||||
const mockDownload: Download = {
|
||||
id: 'id1',
|
||||
title: 'Test',
|
||||
url: 'url1',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'finished',
|
||||
msg: '',
|
||||
filename: 'test.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 100,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
checked: false,
|
||||
};
|
||||
service.done.set('url1', mockDownload);
|
||||
|
||||
const ids = ['url1'];
|
||||
service.delById('done', ids).subscribe();
|
||||
|
||||
expect(mockDownload.deleting).toBe(true);
|
||||
|
||||
const req = httpMock.expectOne('delete');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ where: 'done', ids });
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('should start downloads by filter', () => {
|
||||
const mockDownload1: Download = {
|
||||
id: 'id1',
|
||||
title: 'Test 1',
|
||||
url: 'url1',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
filename: 'test1.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
checked: false,
|
||||
};
|
||||
const mockDownload2: Download = {
|
||||
id: 'id2',
|
||||
title: 'Test 2',
|
||||
url: 'url2',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'downloading',
|
||||
msg: '',
|
||||
filename: 'test2.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 50,
|
||||
speed: 1000,
|
||||
eta: 10,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
service.queue.set('url1', mockDownload1);
|
||||
service.queue.set('url2', mockDownload2);
|
||||
|
||||
service.startByFilter('queue', (dl) => dl.status === 'pending').subscribe();
|
||||
|
||||
const req = httpMock.expectOne('start');
|
||||
expect(req.request.body.ids).toEqual(['url1']);
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('should delete downloads by filter', () => {
|
||||
const mockDownload1: Download = {
|
||||
id: 'id1',
|
||||
title: 'Test 1',
|
||||
url: 'url1',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'finished',
|
||||
msg: '',
|
||||
filename: 'test1.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 100,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
checked: false,
|
||||
};
|
||||
const mockDownload2: Download = {
|
||||
id: 'id2',
|
||||
title: 'Test 2',
|
||||
url: 'url2',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'error',
|
||||
msg: 'Failed',
|
||||
filename: 'test2.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
service.done.set('url1', mockDownload1);
|
||||
service.done.set('url2', mockDownload2);
|
||||
|
||||
service.delByFilter('done', (dl) => dl.status === 'error').subscribe();
|
||||
|
||||
const req = httpMock.expectOne('delete');
|
||||
expect(req.request.body.ids).toEqual(['url2']);
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('should handle HTTP errors', () => {
|
||||
const url = 'https://example.com/video';
|
||||
|
||||
service.add(url, 'best', 'mp4', '', '', false, 0, true).subscribe((response) => {
|
||||
expect(response.status).toBe('error');
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne('add');
|
||||
req.error(new ProgressEvent('error'), {
|
||||
status: 500,
|
||||
statusText: 'Server Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add download by URL with default values', async () => {
|
||||
const url = 'https://example.com/video';
|
||||
const mockStatus: Status = { status: 'success' };
|
||||
|
||||
const promise = service.addDownloadByUrl(url);
|
||||
|
||||
const req = httpMock.expectOne('add');
|
||||
expect(req.request.body).toEqual({
|
||||
url,
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
auto_start: true,
|
||||
});
|
||||
req.flush(mockStatus);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual(mockStatus);
|
||||
});
|
||||
|
||||
it('should export queue URLs', () => {
|
||||
const mockDownload1: Download = {
|
||||
id: 'id1',
|
||||
title: 'Test 1',
|
||||
url: 'https://example.com/video1',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
filename: 'test1.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
checked: false,
|
||||
};
|
||||
const mockDownload2: Download = {
|
||||
id: 'id2',
|
||||
title: 'Test 2',
|
||||
url: 'https://example.com/video2',
|
||||
quality: 'best',
|
||||
format: 'mp4',
|
||||
status: 'downloading',
|
||||
msg: '',
|
||||
filename: 'test2.mp4',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_strict_mode: false,
|
||||
playlist_item_limit: 0,
|
||||
percent: 50,
|
||||
speed: 1000,
|
||||
eta: 10,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
service.queue.set('url1', mockDownload1);
|
||||
service.queue.set('url2', mockDownload2);
|
||||
|
||||
const urls = service.exportQueueUrls();
|
||||
expect(urls).toEqual(['https://example.com/video1', 'https://example.com/video2']);
|
||||
});
|
||||
});
|
||||
81
ui/src/app/services/speed.service.spec.ts
Normal file
81
ui/src/app/services/speed.service.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { SpeedService } from './speed.service';
|
||||
|
||||
describe('SpeedService', () => {
|
||||
let service: SpeedService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [SpeedService],
|
||||
});
|
||||
service = TestBed.inject(SpeedService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return 0 for mean speed when no measurements are added', () => {
|
||||
const currentSpeed = service.getCurrentMeanSpeed();
|
||||
expect(currentSpeed).toBe(0);
|
||||
});
|
||||
|
||||
it('should add speed measurement and calculate mean', () => {
|
||||
service.addSpeedMeasurement(100);
|
||||
service.addSpeedMeasurement(200);
|
||||
service.addSpeedMeasurement(300);
|
||||
|
||||
const meanSpeed = service.getCurrentMeanSpeed();
|
||||
expect(meanSpeed).toBe(200); // (100 + 200 + 300) / 3 = 200
|
||||
});
|
||||
|
||||
it('should maintain buffer size of 10 measurements', () => {
|
||||
// Add 15 measurements
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
service.addSpeedMeasurement(i * 10);
|
||||
}
|
||||
|
||||
// Should only keep the last 10: 60, 70, 80, 90, 100, 110, 120, 130, 140, 150
|
||||
const meanSpeed = service.getCurrentMeanSpeed();
|
||||
const expected = (60 + 70 + 80 + 90 + 100 + 110 + 120 + 130 + 140 + 150) / 10;
|
||||
expect(meanSpeed).toBe(expected);
|
||||
});
|
||||
|
||||
it('should provide a mean speed observable', () => {
|
||||
service.addSpeedMeasurement(100);
|
||||
service.addSpeedMeasurement(200);
|
||||
|
||||
// Verify the observable exists and is defined
|
||||
expect(service.meanSpeed$).toBeDefined();
|
||||
|
||||
// We can't easily test the interval-based observable in a unit test
|
||||
// without complex zone/fakeAsync setup, so we verify behavior through
|
||||
// the getCurrentMeanSpeed() method instead
|
||||
const currentMean = service.getCurrentMeanSpeed();
|
||||
expect(currentMean).toBe(150); // (100 + 200) / 2 = 150
|
||||
});
|
||||
|
||||
it('should handle single measurement', () => {
|
||||
service.addSpeedMeasurement(500);
|
||||
const meanSpeed = service.getCurrentMeanSpeed();
|
||||
expect(meanSpeed).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle zero speeds', () => {
|
||||
service.addSpeedMeasurement(0);
|
||||
service.addSpeedMeasurement(0);
|
||||
const meanSpeed = service.getCurrentMeanSpeed();
|
||||
expect(meanSpeed).toBe(0);
|
||||
});
|
||||
|
||||
it('should update mean speed after new measurements', () => {
|
||||
service.addSpeedMeasurement(100);
|
||||
expect(service.getCurrentMeanSpeed()).toBe(100);
|
||||
|
||||
service.addSpeedMeasurement(300);
|
||||
expect(service.getCurrentMeanSpeed()).toBe(200);
|
||||
|
||||
service.addSpeedMeasurement(600);
|
||||
expect(service.getCurrentMeanSpeed()).toBe(333.3333333333333);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue