From 50e28dd05139a8fc35a9036dccdaefbdef425dbe Mon Sep 17 00:00:00 2001 From: Mojtaba Erfan Rad Date: Mon, 9 Sep 2024 13:57:12 +0330 Subject: [PATCH] fix(login-form): added tests --- .../login-form/login-form.component.html | 14 +-- .../login-form/login-form.component.spec.ts | 98 ++++++++++++++++++- .../login/login-form/login-form.component.ts | 11 +-- src/app/user/services/auth/auth.service.ts | 6 +- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/src/app/user/components/login/login-form/login-form.component.html b/src/app/user/components/login/login-form/login-form.component.html index 0f35ba7..e184a5b 100644 --- a/src/app/user/components/login/login-form/login-form.component.html +++ b/src/app/user/components/login/login-form/login-form.component.html @@ -15,18 +15,18 @@

Login

placeholder="at least 8 chracters" [(ngModel)]="password" matInput - [type]="hide() ? 'password' : 'text'" + [type]="hide ? 'password' : 'text'" name="password" /> Login [disabled]="isLoading" (click)="loginClick()" > - @if (isLoading) { - Logging in... - } @else { - Login - } + @if (isLoading) { Logging in... } @else { Login }

diff --git a/src/app/user/components/login/login-form/login-form.component.spec.ts b/src/app/user/components/login/login-form/login-form.component.spec.ts index c361565..afd544b 100644 --- a/src/app/user/components/login/login-form/login-form.component.spec.ts +++ b/src/app/user/components/login/login-form/login-form.component.spec.ts @@ -10,12 +10,33 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { AuthService } from '../../../services/auth/auth.service'; +import { of, throwError } from 'rxjs'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { LoadingService } from '../../../../shared/services/loading.service'; +import { DangerSuccessNotificationComponent } from '../../../../shared/components/danger-success-notification/danger-success-notification.component'; -describe('LoginFormComponent', () => { +fdescribe('LoginFormComponent', () => { let component: LoginFormComponent; let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockMatSnackBar: jasmine.SpyObj; + let mockLoadingService: jasmine.SpyObj; + + const loginInfo = { + username: 'mamad', + password: 'M@mad123', + rememberMe: true, + }; beforeEach(async () => { + mockRouter = jasmine.createSpyObj(['navigate']); + mockAuthService = jasmine.createSpyObj(['login']); + mockMatSnackBar = jasmine.createSpyObj(['openFromComponent']); + mockLoadingService = jasmine.createSpyObj(['setLoading']); + await TestBed.configureTestingModule({ declarations: [LoginFormComponent], imports: [ @@ -27,7 +48,14 @@ describe('LoginFormComponent', () => { MatButtonModule, BrowserAnimationsModule, ], - providers: [provideHttpClient(), provideHttpClientTesting()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: AuthService, useValue: mockAuthService }, + { provide: MatSnackBar, useValue: mockMatSnackBar }, + { provide: Router, useValue: mockRouter }, + { provide: LoadingService, useValue: mockLoadingService }, + ], }).compileComponents(); fixture = TestBed.createComponent(LoginFormComponent); @@ -42,10 +70,10 @@ describe('LoginFormComponent', () => { it('should bind username and password inputs', () => { fixture.whenStable().then(() => { const usernameInput = fixture.nativeElement.querySelector( - 'input[name="userName"]', + 'input[name="userName"]' ); const passwordInput = fixture.nativeElement.querySelector( - 'input[name="password"]', + 'input[name="password"]' ); usernameInput.value = 'testUser'; @@ -73,4 +101,66 @@ describe('LoginFormComponent', () => { const button = fixture.nativeElement.querySelector('button[type="submit"]'); expect(button.disabled).toBeTruthy(); }); + + it('SHOULD show message and redirect to dashboard WHEN login successfully', () => { + mockAuthService.login.and.returnValue(of()); + component.username = loginInfo.username; + component.password = loginInfo.password; + component.checked = loginInfo.rememberMe; + + component.loginClick(); + + expect(mockAuthService.login).toHaveBeenCalledWith({ + username: loginInfo.username, + password: loginInfo.password, + rememberMe: loginInfo.rememberMe, + }); + + // expect(mockMatSnackBar.openFromComponent).toHaveBeenCalledWith( + // DangerSuccessNotificationComponent, + // { + // data: 'Logged in successfully.', + // panelClass: ['notification-class-success'], + // duration: 2000, + // } + // ); + // expect(mockRouter.navigate).toHaveBeenCalledWith(['/dashboard']); + }); + + it('SHOULD give error WHEN login fails', () => { + const mockError = { error: { message: 'Update failed' } }; + + mockAuthService.login.and.returnValue(throwError(() => mockError)); + + component.username = loginInfo.username; + component.password = loginInfo.password; + component.checked = loginInfo.rememberMe; + + component.loginClick(); + + expect(mockAuthService.login).toHaveBeenCalledWith({ + username: loginInfo.username, + password: loginInfo.password, + rememberMe: loginInfo.rememberMe, + }); + + expect(mockMatSnackBar.openFromComponent).toHaveBeenCalledWith( + DangerSuccessNotificationComponent, + { + data: mockError.error.message, + panelClass: ['notification-class-danger'], + duration: 2000, + } + ); + expect(mockLoadingService.setLoading).toHaveBeenCalledWith(false); + }); + + it('should toggle hide property and prevent event propagation', () => { + // Arrange + component.hide = false; + // Act + component.hidePassClick(); + // Assert + expect(component.hide).toBeTrue(); + }); }); diff --git a/src/app/user/components/login/login-form/login-form.component.ts b/src/app/user/components/login/login-form/login-form.component.ts index d55eeed..30080fe 100644 --- a/src/app/user/components/login/login-form/login-form.component.ts +++ b/src/app/user/components/login/login-form/login-form.component.ts @@ -1,4 +1,4 @@ -import { Component, signal } from '@angular/core'; +import { Component } from '@angular/core'; import { LoginRequest } from '../../../models/User'; import { DangerSuccessNotificationComponent } from '../../../../shared/components/danger-success-notification/danger-success-notification.component'; import { AuthService } from '../../../services/auth/auth.service'; @@ -12,7 +12,7 @@ import { LoadingService } from '../../../../shared/services/loading.service'; styleUrl: './login-form.component.scss', }) export class LoginFormComponent { - hide = signal(true); + hide = true; checked = false; username = ''; password = ''; @@ -22,7 +22,7 @@ export class LoginFormComponent { private authService: AuthService, private router: Router, private _snackBar: MatSnackBar, - private loadingService: LoadingService, + private loadingService: LoadingService ) { this.loadingService.setLoading(false); } @@ -58,8 +58,7 @@ export class LoginFormComponent { }); } - hidePassClick(event: MouseEvent) { - this.hide.set(!this.hide()); - event.stopPropagation(); + hidePassClick() { + this.hide = !this.hide; } } diff --git a/src/app/user/services/auth/auth.service.ts b/src/app/user/services/auth/auth.service.ts index 3585810..77223d8 100644 --- a/src/app/user/services/auth/auth.service.ts +++ b/src/app/user/services/auth/auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable, tap } from 'rxjs'; +import { BehaviorSubject, tap } from 'rxjs'; import { LoginRequest, UserPermissions } from '../../models/User'; import { environment } from '../../../../../api-config/api-url'; import { LoadingService } from '../../../shared/services/loading.service'; @@ -18,9 +18,9 @@ export class AuthService { private loadingService: LoadingService ) {} - login(loginRequest: LoginRequest): Observable { + login(loginRequest: LoginRequest) { this.loadingService.setLoading(true); - return this.http.post(this.apiUrl + '/login', loginRequest, { + return this.http.post(this.apiUrl + '/login', loginRequest, { withCredentials: true, }); }