import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { of, throwError } from 'rxjs'; import { LoginComponent } from './login.component'; import { AuthService } from '../auth/auth.service'; describe('LoginComponent', () => { let component: LoginComponent; let authService: jasmine.SpyObj; let router: jasmine.SpyObj; beforeEach(async () => { authService = jasmine.createSpyObj('AuthService', ['login']); router = jasmine.createSpyObj('Router', ['navigate', 'navigateByUrl']); await TestBed.configureTestingModule({ imports: [LoginComponent, ReactiveFormsModule], providers: [ { provide: AuthService, useValue: authService }, { provide: Router, useValue: router }, ], }).compileComponents(); const fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('submit calls AuthService.login with username and password', fakeAsync(() => { authService.login.and.returnValue(of(undefined)); component.form.setValue({ username: 'owner', password: 'hunter2' }); component.onSubmit(); tick(); expect(authService.login).toHaveBeenCalledWith('owner', 'hunter2'); })); it('navigates to library on success', fakeAsync(() => { authService.login.and.returnValue(of(undefined)); router.navigateByUrl.and.returnValue(Promise.resolve(true)); component.form.setValue({ username: 'owner', password: 'hunter2' }); component.onSubmit(); tick(); expect(router.navigateByUrl).toHaveBeenCalledWith('/'); })); it('shows error message on 401', fakeAsync(() => { const err = new HttpErrorResponse({ status: 401 }); authService.login.and.returnValue(throwError(() => err)); component.form.setValue({ username: 'owner', password: 'wrong' }); component.onSubmit(); tick(); expect(component.errorMessage).toBeTruthy(); })); it('does not call login when fields are empty', fakeAsync(() => { component.form.setValue({ username: '', password: '' }); component.onSubmit(); tick(); expect(authService.login).not.toHaveBeenCalled(); })); // New polish tests it('submit button shows "Signing in…" and is disabled while loading', () => { const fixture = TestBed.createComponent(LoginComponent); const comp = fixture.componentInstance; fixture.detectChanges(); comp.loading = true; fixture.detectChanges(); const btn = (fixture.nativeElement as HTMLElement).querySelector('button[type="submit"]') as HTMLButtonElement; expect(btn.textContent?.trim()).toContain('Signing in'); expect(btn.disabled).toBeTrue(); }); it('field-level validation error shown for empty username on touched', () => { const fixture = TestBed.createComponent(LoginComponent); const comp = fixture.componentInstance; fixture.detectChanges(); comp.form.get('username')!.markAsTouched(); fixture.detectChanges(); const err = (fixture.nativeElement as HTMLElement).querySelector('.validation-error'); expect(err).not.toBeNull(); }); it('errorMessage paragraph is visible when errorMessage is set', fakeAsync(() => { authService.login.and.returnValue(throwError(() => new HttpErrorResponse({ status: 401 }))); component.form.setValue({ username: 'owner', password: 'wrong' }); component.onSubmit(); tick(); const fixture = TestBed.createComponent(LoginComponent); fixture.componentInstance.errorMessage = component.errorMessage; fixture.detectChanges(); const errPara = (fixture.nativeElement as HTMLElement).querySelector('.error-message'); expect(errPara).not.toBeNull(); })); it('fields retain their values after a failed login', fakeAsync(() => { authService.login.and.returnValue(throwError(() => new HttpErrorResponse({ status: 401 }))); component.form.setValue({ username: 'owner', password: 'wrong' }); component.onSubmit(); tick(); expect(component.form.value.username).toBe('owner'); expect(component.form.value.password).toBe('wrong'); })); });