Skip to content

Commit

Permalink
fix(login-form): added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mojerf committed Sep 9, 2024
1 parent 9ff46c5 commit 50e28dd
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ <h2>Login</h2>
placeholder="at least 8 chracters"
[(ngModel)]="password"
matInput
[type]="hide() ? 'password' : 'text'"
[type]="hide ? 'password' : 'text'"
name="password"
/>
<button
mat-icon-button
matSuffix
(click)="hidePassClick($event)"
(click)="hidePassClick()"
[attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hide()"
[attr.aria-pressed]="hide"
type="button"
>
<mat-icon>{{ hide() ? "visibility_off" : "visibility" }}</mat-icon>
<mat-icon>{{ hide ? "visibility_off" : "visibility" }}</mat-icon>
</button>
</mat-form-field>
<mat-checkbox [(ngModel)]="checked" name="rememberMe"
Expand All @@ -38,11 +38,7 @@ <h2>Login</h2>
[disabled]="isLoading"
(click)="loginClick()"
>
@if (isLoading) {
Logging in...
} @else {
Login
}
@if (isLoading) { Logging in... } @else { Login }
</button>

<p class="forget">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoginFormComponent>;
let mockAuthService: jasmine.SpyObj<AuthService>;
let mockRouter: jasmine.SpyObj<Router>;
let mockMatSnackBar: jasmine.SpyObj<MatSnackBar>;
let mockLoadingService: jasmine.SpyObj<LoadingService>;

const loginInfo = {
username: 'mamad',
password: 'M@mad123',
rememberMe: true,
};

beforeEach(async () => {
mockRouter = jasmine.createSpyObj<Router>(['navigate']);
mockAuthService = jasmine.createSpyObj<AuthService>(['login']);
mockMatSnackBar = jasmine.createSpyObj<MatSnackBar>(['openFromComponent']);
mockLoadingService = jasmine.createSpyObj<LoadingService>(['setLoading']);

await TestBed.configureTestingModule({
declarations: [LoginFormComponent],
imports: [
Expand All @@ -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);
Expand All @@ -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';
Expand Down Expand Up @@ -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();
});
});
11 changes: 5 additions & 6 deletions src/app/user/components/login/login-form/login-form.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = '';
Expand All @@ -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);
}
Expand Down Expand Up @@ -58,8 +58,7 @@ export class LoginFormComponent {
});
}

hidePassClick(event: MouseEvent) {
this.hide.set(!this.hide());
event.stopPropagation();
hidePassClick() {
this.hide = !this.hide;
}
}
6 changes: 3 additions & 3 deletions src/app/user/services/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,9 +18,9 @@ export class AuthService {
private loadingService: LoadingService
) {}

login(loginRequest: LoginRequest): Observable<void> {
login(loginRequest: LoginRequest) {
this.loadingService.setLoading(true);
return this.http.post<void>(this.apiUrl + '/login', loginRequest, {
return this.http.post(this.apiUrl + '/login', loginRequest, {
withCredentials: true,
});
}
Expand Down

0 comments on commit 50e28dd

Please sign in to comment.