diff --git a/src/app/features/accounting/charges/charges-list.component.spec.ts b/src/app/features/accounting/charges/charges-list.component.spec.ts new file mode 100644 index 000000000..006dedaf1 --- /dev/null +++ b/src/app/features/accounting/charges/charges-list.component.spec.ts @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; + +import { ChargesListComponent } from './charges-list.component'; +import { ChargesService } from '../../../api'; +import { provideIonicTesting } from '../../../testing/ionic-testing'; +import { provideTranslateTesting } from '../../../testing/i18n-testing'; + +describe('ChargesListComponent', () => { + let component: ChargesListComponent; + let fixture: ComponentFixture; + let serviceSpy: jasmine.SpyObj; + + const charges = [{ id: 1, name: 'Processing fee', amount: 10 }]; + + beforeEach(async () => { + serviceSpy = jasmine.createSpyObj('ChargesService', ['getCharges']); + serviceSpy.getCharges.and.returnValue( + of(charges) as unknown as ReturnType, + ); + + await TestBed.configureTestingModule({ + imports: [ChargesListComponent], + providers: [ + provideNoopAnimations(), + provideIonicTesting(), + ...provideTranslateTesting(), + { provide: ChargesService, useValue: serviceSpy }, + { provide: Router, useValue: jasmine.createSpyObj('Router', ['navigate']) }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ChargesListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('loads charges on init', () => { + expect(serviceSpy.getCharges).toHaveBeenCalledTimes(1); + expect(component.charges()).toEqual(charges); + expect(component.hasError()).toBeFalse(); + }); + + it('shows a load error instead of an empty table', () => { + serviceSpy.getCharges.and.returnValue( + throwError(() => new Error('boom')) as unknown as ReturnType, + ); + + component.onRetry(); + fixture.detectChanges(); + + expect(component.hasError()).toBeTrue(); + expect(component.charges()).toEqual([]); + expect(fixture.nativeElement.querySelector('[data-testid="data-table-error"]')).not.toBeNull(); + }); + + it('clears the error after a successful retry', () => { + serviceSpy.getCharges.and.returnValue( + throwError(() => new Error('boom')) as unknown as ReturnType, + ); + component.onRetry(); + expect(component.hasError()).toBeTrue(); + + serviceSpy.getCharges.and.returnValue( + of(charges) as unknown as ReturnType, + ); + + component.onRetry(); + + expect(component.hasError()).toBeFalse(); + expect(component.charges()).toEqual(charges); + }); +}); diff --git a/src/app/features/accounting/charges/charges-list.component.ts b/src/app/features/accounting/charges/charges-list.component.ts index 31cd1149b..bf331d0a5 100644 --- a/src/app/features/accounting/charges/charges-list.component.ts +++ b/src/app/features/accounting/charges/charges-list.component.ts @@ -53,9 +53,11 @@ import { IonButton, IonIcon } from '@ionic/angular/standalone'; [columns]="columns" [data]="charges()" [totalRecords]="charges().length" + [hasError]="hasError()" [showSearch]="true" [localLogic]="true" (create)="onCreateCharge()" + (retry)="onRetry()" > @if ( @@ -103,6 +105,7 @@ export class ChargesListComponent implements OnInit { ]; readonly charges = signal([]); + readonly hasError = signal(false); ngOnInit(): void { this.loadCharges(); @@ -111,12 +114,20 @@ export class ChargesListComponent implements OnInit { private loadCharges(): void { this.chargesService.getCharges().subscribe({ next: (data) => { + this.hasError.set(false); this.charges.set(data || []); }, - error: (err) => console.error('Failed to load charges', err), + error: () => { + this.hasError.set(true); + this.charges.set([]); + }, }); } + onRetry(): void { + this.loadCharges(); + } + onCreateCharge(): void { this.router.navigate(['/accounting/charges/create']); }