From 34512f3b9add04918ed042bdca41b99fa44fd5a0 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Wed, 2 Apr 2025 11:09:25 +0200 Subject: [PATCH] Set up Jest configuration and update dependencies for testing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚙️ **Config**: Added Jest configuration files for testing - 🗑️ **Chore**: Removed unused Karma configuration and assets - 🛠️ **Refactor**: Updated return details store methods for better clarity - 📚 **Docs**: Enhanced comments and documentation in return details store --- jest.config.js | 5 + junit.xml | 85 ++++++++++ karma.conf.js | 58 ------- karma/assets/unit-test.svg | 2 - libs/oms/data-access/jest.config.ts | 10 ++ .../src/lib/return-details.store.spec.ts | 147 ++++++++++++++++++ .../src/lib/return-details.store.ts | 71 +++++++-- package-lock.json | 62 ++++---- package.json | 16 +- 9 files changed, 340 insertions(+), 116 deletions(-) create mode 100644 jest.config.js create mode 100644 junit.xml delete mode 100644 karma.conf.js delete mode 100644 karma/assets/unit-test.svg create mode 100644 libs/oms/data-access/src/lib/return-details.store.spec.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..649f8a6e5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +const nxPreset = require('@nx/jest/preset').default; + +module.exports = { + ...nxPreset, +}; diff --git a/junit.xml b/junit.xml new file mode 100644 index 000000000..49ec46595 --- /dev/null +++ b/junit.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index d2f00ed92..000000000 --- a/karma.conf.js +++ /dev/null @@ -1,58 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - const testParamIndex = process.argv.findIndex((arg) => arg === 'test'); - - let project = process.argv[testParamIndex + 1]; - - project = project.replace('@', '').replace('/', '-'); - - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('karma-junit-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - client: { - clearContext: false, // leave Jasmine Spec Runner output visible in browser - }, - jasmineHtmlReporter: { - suppressAll: true, // removes the duplicated traces - }, - - reporters: ['progress', 'junit', 'kjhtml'], - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - - coverageReporter: { - dir: require('path').join(__dirname, 'coverage', project), - subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'cobertura', file: 'cobertura.xml' }], - // include all files - includeAllSources: true, - }, - junitReporter: { - outputDir: require('path').join(__dirname, 'testresults', project), - suite: project, - useBrowserName: false, - properties: { - 'project.name': project, - }, - }, - customLaunchers: { - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'], - }, - }, - files: [{ pattern: 'karma/assets/**/*', watched: false, included: false, served: true, nocache: false }], - }); -}; diff --git a/karma/assets/unit-test.svg b/karma/assets/unit-test.svg deleted file mode 100644 index fbe35029a..000000000 --- a/karma/assets/unit-test.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/libs/oms/data-access/jest.config.ts b/libs/oms/data-access/jest.config.ts index 024724b93..30d3c2c7a 100644 --- a/libs/oms/data-access/jest.config.ts +++ b/libs/oms/data-access/jest.config.ts @@ -18,4 +18,14 @@ export default { 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment', ], + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'testresults', + outputName: `TEST-oms-data-access.xml`, + }, + ], + ], }; diff --git a/libs/oms/data-access/src/lib/return-details.store.spec.ts b/libs/oms/data-access/src/lib/return-details.store.spec.ts new file mode 100644 index 000000000..401abae1a --- /dev/null +++ b/libs/oms/data-access/src/lib/return-details.store.spec.ts @@ -0,0 +1,147 @@ +import { createServiceFactory } from '@ngneat/spectator/jest'; +import { ReturnDetailsStore } from './return-details.store'; +import { ReturnDetailsService } from './return-details.service'; +import { patchState } from '@ngrx/signals'; +import { ResultStatus } from '@isa/common/result'; +import { addEntity } from '@ngrx/signals/entities'; +import { Receipt } from './models'; +import { of, throwError } from 'rxjs'; + +describe('ReturnDetailsStore', () => { + const createService = createServiceFactory({ + service: ReturnDetailsStore, + mocks: [ReturnDetailsService], + }); + + describe('Initialization', () => { + it('should create an instance of ReturnDetailsStore', () => { + const spectator = createService(); + expect(spectator.service).toBeTruthy(); + }); + }); + + describe('Entity Management', () => { + describe('beforeFetch', () => { + it('should create a new entity and set status to Pending if it does not exist', () => { + const spectator = createService(); + const receiptId = 123; + spectator.service.beforeFetch(receiptId); + + expect(spectator.service.entityMap()[123]).toEqual({ + id: receiptId, + data: undefined, + status: ResultStatus.Pending, + }); + }); + + it('should update the existing entity status to Pending', () => { + const spectator = createService(); + const receiptId = 123; + + const data = {}; + + patchState( + spectator.service as any, + addEntity({ id: receiptId, data, status: ResultStatus.Idle }), + ); + + spectator.service.beforeFetch(receiptId); + + expect(spectator.service.entityMap()[123]).toEqual({ + id: receiptId, + data, + status: ResultStatus.Pending, + }); + }); + }); + + describe('fetchSuccess', () => { + it('should update the entity with fetched data and set status to Success', () => { + const spectator = createService(); + const receiptId = 123; + const data: Receipt = { id: receiptId, items: [], buyer: { buyerNumber: '321' } }; + + patchState( + spectator.service as any, + addEntity({ id: receiptId, data: undefined, status: ResultStatus.Pending }), + ); + + spectator.service.fetchSuccess(receiptId, data); + + expect(spectator.service.entityMap()[123]).toEqual({ + id: receiptId, + data, + status: ResultStatus.Success, + }); + }); + }); + + describe('fetchError', () => { + it('should update the entity status to Error', () => { + const spectator = createService(); + const receiptId = 123; + const error = new Error('Fetch error'); + + patchState( + spectator.service as any, + addEntity({ id: receiptId, data: undefined, status: ResultStatus.Pending }), + ); + + spectator.service.fetchError(receiptId, error); + + const entity = spectator.service.entityMap()[123]; + expect(entity).toMatchObject({ + id: receiptId, + status: ResultStatus.Error, + error, + }); + }); + }); + }); + + describe('fetch', () => { + it('should call the service and update the store on success', () => { + const spectator = createService(); + const receiptId = 123; + const data: Receipt = { id: receiptId, items: [], buyer: { buyerNumber: '321' } }; + + spectator.service.beforeFetch(receiptId); + spectator.inject(ReturnDetailsService).fetchReturnDetails.mockReturnValueOnce(of(data)); + + spectator.service.fetch({ receiptId }); + + expect(spectator.inject(ReturnDetailsService).fetchReturnDetails).toHaveBeenCalledWith({ + receiptId, + }); + + expect(spectator.service.entityMap()[123]).toEqual({ + id: receiptId, + data, + status: ResultStatus.Success, + }); + }); + + it('should handle errors and update the store accordingly', () => { + const spectator = createService(); + const receiptId = 123; + const error = new Error('Fetch error'); + + spectator.service.beforeFetch(receiptId); + spectator + .inject(ReturnDetailsService) + .fetchReturnDetails.mockReturnValueOnce(throwError(() => error)); + + spectator.service.fetch({ receiptId }); + + expect(spectator.inject(ReturnDetailsService).fetchReturnDetails).toHaveBeenCalledWith({ + receiptId, + }); + const entity = spectator.service.entityMap()[123]; + expect(entity).toMatchObject({ + id: receiptId, + status: ResultStatus.Error, + error, + }); + }); + }); +}); diff --git a/libs/oms/data-access/src/lib/return-details.store.ts b/libs/oms/data-access/src/lib/return-details.store.ts index 458f3b2b2..93fa41200 100644 --- a/libs/oms/data-access/src/lib/return-details.store.ts +++ b/libs/oms/data-access/src/lib/return-details.store.ts @@ -1,5 +1,11 @@ import { patchState, signalStore, type, withMethods } from '@ngrx/signals'; -import { addEntity, entityConfig, updateEntity, withEntities } from '@ngrx/signals/entities'; +import { + addEntity, + entityConfig, + setEntity, + updateEntity, + withEntities, +} from '@ngrx/signals/entities'; import { Result, ResultStatus } from '@isa/common/result'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { pipe, switchMap, tap } from 'rxjs'; @@ -8,55 +14,86 @@ import { ReturnDetailsService } from './return-details.service'; import { tapResponse } from '@ngrx/operators'; import { Receipt } from './models'; +/** + * Represents the result of a return operation, including the receipt data and status. + */ export type ReturnResult = Result & { id: number }; +/** + * Initial state for a return result entity, excluding the unique identifier. + */ const initialEntity: Omit = { data: undefined, status: ResultStatus.Idle, }; -const config = entityConfig({ - entity: type(), - selectId: (entity) => entity.id, -}); - +/** + * Store for managing return details using NgRx signals. + * Provides methods for fetching and updating return details. + */ export const ReturnDetailsStore = signalStore( { providedIn: 'root' }, - withEntities(config), + withEntities(), withMethods((store) => ({ - _beforeFetch(receiptId: number) { + /** + * Prepares the store before fetching return details by adding or updating the entity with a pending status. + * @param receiptId - The unique identifier of the receipt. + * @returns The updated or newly created entity. + */ + beforeFetch(receiptId: number) { let entity: ReturnResult | undefined = store.entityMap()[receiptId]; if (!entity) { entity = { ...initialEntity, id: receiptId, status: ResultStatus.Pending }; + patchState(store, addEntity(entity)); + } else { + patchState( + store, + updateEntity({ id: receiptId, changes: { status: ResultStatus.Pending } }), + ); } - patchState(store, addEntity(entity, config)); - return entity; }, - _fetchSuccess(receiptId: number, data: Receipt) { + + /** + * Updates the store with the fetched return details on a successful fetch operation. + * @param receiptId - The unique identifier of the receipt. + * @param data - The fetched receipt data. + */ + fetchSuccess(receiptId: number, data: Receipt) { patchState( store, - updateEntity({ id: receiptId, changes: { data, status: ResultStatus.Success } }, config), + updateEntity({ id: receiptId, changes: { data, status: ResultStatus.Success } }), ); }, - _fetchError(receiptId: number, error: unknown) { + + /** + * Updates the store with an error state if the fetch operation fails. + * @param receiptId - The unique identifier of the receipt. + * @param error - The error encountered during the fetch operation. + */ + fetchError(receiptId: number, error: unknown) { patchState( store, - updateEntity({ id: receiptId, changes: { error, status: ResultStatus.Error } }, config), + updateEntity({ id: receiptId, changes: { error, status: ResultStatus.Error } }), ); }, })), withMethods((store, returnDetailsService = inject(ReturnDetailsService)) => ({ + /** + * Fetches return details for a given receipt ID. + * Updates the store with the appropriate state based on the fetch result. + * @param params - An object containing the receipt ID. + */ fetch: rxMethod<{ receiptId: number }>( pipe( - tap(({ receiptId }) => store._beforeFetch(receiptId)), + tap(({ receiptId }) => store.beforeFetch(receiptId)), switchMap(({ receiptId }) => returnDetailsService.fetchReturnDetails({ receiptId }).pipe( tapResponse({ next(value) { - store._fetchSuccess(receiptId, value); + store.fetchSuccess(receiptId, value); }, error(error) { - store._fetchError(receiptId, error); + store.fetchError(receiptId, error); }, }), ), diff --git a/package-lock.json b/package-lock.json index 5d49bc509..678805a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,13 +22,13 @@ "@microsoft/signalr": "^8.0.7", "@ng-icons/core": "^29.5.1", "@ng-icons/material-icons": "^29.5.1", - "@ngrx/component-store": "19.0.1", - "@ngrx/effects": "19.0.1", - "@ngrx/entity": "19.0.1", - "@ngrx/operators": "19.0.1", - "@ngrx/signals": "^19.0.1", - "@ngrx/store": "19.0.1", - "@ngrx/store-devtools": "19.0.1", + "@ngrx/component-store": "19.1.0", + "@ngrx/effects": "19.1.0", + "@ngrx/entity": "19.1.0", + "@ngrx/operators": "19.1.0", + "@ngrx/signals": "19.1.0", + "@ngrx/store": "19.1.0", + "@ngrx/store-devtools": "19.1.0", "angular-oauth2-oidc": "^17.0.2", "angular-oauth2-oidc-jwks": "^17.0.2", "lodash": "^4.17.21", @@ -7077,9 +7077,9 @@ } }, "node_modules/@ngrx/component-store": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-19.0.1.tgz", - "integrity": "sha512-H/UNz7TMVWF8RUNWwEhfGy6zR5vfMEnGlxcAr+CwxRTryx9H0dsBE6esM/Kj/Po9EOjMFnjiY32ccyOrPUfFWA==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-19.1.0.tgz", + "integrity": "sha512-CYgmYlSWrNEUiP+5LHA05RhEpqkomFv+C3w0GhpcFcK4dYzipZY7BSJLaYdMCM+Q1Ce3FwBkEMgSSUycL0B5zw==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -7090,37 +7090,37 @@ } }, "node_modules/@ngrx/effects": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.0.1.tgz", - "integrity": "sha512-q+eztS1zN1247BtUZ41gxhumj4wMmvtfdSMfkFEuu6zuA57Vbx8zitEsw9boqPGtP5E4Cj5HKJLSJrl2kgwgcQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.1.0.tgz", + "integrity": "sha512-rGRN0ZnzAPmQdUPvmoqZsK7Da/AoCfQcfody+h6PfHTwXNm+M2MRc8tXO6C+fznMRww8ZgNror2dfFmoOSOvNg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@angular/core": "^19.0.0", - "@ngrx/store": "19.0.1", + "@ngrx/store": "19.1.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/entity": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-19.0.1.tgz", - "integrity": "sha512-Dw6UhLi7tGVWb/pLgYI81k1fPxCIbCWMztGKj08e8fLWoMvTWYTbG5tFbOJNSa9D3gPmxsBbbS8VMNqcUgl7wQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-19.1.0.tgz", + "integrity": "sha512-Cy+uT5Lzs0fSaqcAtsK6ECac2ETma7UFnSnqLXSKPQmiBMNMMvP2On1c+zxaPgySG2R5kxgA0yDuHGsRiLAprA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@angular/core": "^19.0.0", - "@ngrx/store": "19.0.1", + "@ngrx/store": "19.1.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/operators": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-19.0.1.tgz", - "integrity": "sha512-4CA+VexfK6nkRb6glmyCSoQgU7zQpEgMF0wVDamxyCO8hJo6E4TwUAN2W5tE5cxWFAsS0+wpFXFncpigvPL9Vw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-19.1.0.tgz", + "integrity": "sha512-gpo69FnoAF69X68pk9eWFHB630xqerBYkF68wOFMciLOV2im3b/fAf+0sRvnQJmVtG/8jO0IPVdvLqa1TbWmPA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -7130,9 +7130,9 @@ } }, "node_modules/@ngrx/signals": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-19.0.1.tgz", - "integrity": "sha512-e9cGgF//tIyN1PKDDcBQkI0csxRcw4r9ezTtDzQpM2gPU5frD9JxaW/YU5gM02ZMl97bUMoI82fBtnDN0RtyWg==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-19.1.0.tgz", + "integrity": "sha512-v8sbb+Iox9kdIaKbFgt4Z1W+NxzIU4+g+6qQU6/c27UmtQXv0s1zUKKofPRK0qwkaZzNWkxNToxyoE285ukqcQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -7148,9 +7148,9 @@ } }, "node_modules/@ngrx/store": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.0.1.tgz", - "integrity": "sha512-+6eBLb+0rdJ856JRuKnvSzFxv1ISbYuX/OM12dMPf4wm+ddxjhyvi6tF8lPiNnaYb717PGNxXQzBFIGfIs4zGQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.1.0.tgz", + "integrity": "sha512-8kKCSFahTpRTx3f/wwcDjItdFnk2IMoorWRjTI2U/MGWuEi4flqLNWcX99s759e7TI6PctiGsaS8jnJXIUS8Jg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -7161,16 +7161,16 @@ } }, "node_modules/@ngrx/store-devtools": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.0.1.tgz", - "integrity": "sha512-afvr6NHh12LpYrOH9JKKEEi/z2DHq/wJ45hRn3mrcRDArQTKpTl7V2eu0dYgjGItSRSeJ2drzzKGjccL61PPSg==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.1.0.tgz", + "integrity": "sha512-c8sV5FQofqm7lF6HTJ4Bb7L/69TaIYHAwEFMbgqbsNoWDx+pilw/It6X9J3LnU8bXjQK4xg6qsgJ7fJIH5X2NA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@angular/core": "^19.0.0", - "@ngrx/store": "19.0.1", + "@ngrx/store": "19.1.0", "rxjs": "^6.5.3 || ^7.5.0" } }, diff --git a/package.json b/package.json index f67f82390..13376730e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "ng": "ng", "start": "nx serve isa-app --ssl", "test": "nx test isa-app", - "ci": "nx test isa-app --watch=false --browsers=ChromeHeadless --code-coverage", + "ci": "npx nx run-many -t test --exclude isa-app -c ci", "build": "nx build isa-app --configuration=development", "build-prod": "nx build isa-app --configuration=production", "lint": "nx lint", @@ -33,13 +33,13 @@ "@microsoft/signalr": "^8.0.7", "@ng-icons/core": "^29.5.1", "@ng-icons/material-icons": "^29.5.1", - "@ngrx/component-store": "19.0.1", - "@ngrx/effects": "19.0.1", - "@ngrx/entity": "19.0.1", - "@ngrx/operators": "19.0.1", - "@ngrx/signals": "^19.0.1", - "@ngrx/store": "19.0.1", - "@ngrx/store-devtools": "19.0.1", + "@ngrx/component-store": "19.1.0", + "@ngrx/effects": "19.1.0", + "@ngrx/entity": "19.1.0", + "@ngrx/operators": "19.1.0", + "@ngrx/signals": "19.1.0", + "@ngrx/store": "19.1.0", + "@ngrx/store-devtools": "19.1.0", "angular-oauth2-oidc": "^17.0.2", "angular-oauth2-oidc-jwks": "^17.0.2", "lodash": "^4.17.21",