diff --git a/karma.conf.js b/karma.conf.js index 890f3743..6325328a 100755 --- a/karma.conf.js +++ b/karma.conf.js @@ -62,4 +62,4 @@ module.exports = function (config) { browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'], restartOnFileChange: true }); -}; +}; \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts old mode 100755 new mode 100644 index 038d38fc..8b0182e4 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -23,16 +23,15 @@ import { PrivacyPolicyComponent } from './pages/privacy-policy/privacy-policy.co import { HomeAdminComponent } from './pages/home-admin/home-admin.component'; import { AdminActivateComponent } from './pages/admin-activate/admin-activate.component'; -import { SuperAdminActivateComponent } from './pages/super-admin-activate/super-admin-activate.component'; import { CategoryTableComponent } from './pages/category-table/category-table.component'; import { VideoViewsComponent } from './pages/video-views/video-views.component'; import { RecordComponent } from './pages/record/record.component'; import { DashboardCategoryComponent } from './pages/dashboard-category/dashboard-category.component'; -import { ControleSuperAdminComponent } from './pages/controle-super-admin/controle-super-admin.component'; +import { NotificationsComponent } from './pages/notifications/notifications.component'; import { WithTokenGuard } from './guard/with-token.guard'; import { TokenAdminGuard } from './guard/admin.guard'; -import { TokenSuperAdminGuard } from './guard/super-admin.guard'; + const routes: Routes = [ { path: '', component: LoginComponent, canActivate: [WithTokenGuard] }, @@ -95,26 +94,22 @@ const routes: Routes = [ canActivate: [AdminGuard], }, { path: 'privacy', component: PrivacyPolicyComponent }, - { - path: 'homeAdmin', + + { path: 'homeAdmin', component: HomeAdminComponent, canActivate: [TokenAdminGuard], }, { path: 'adminActivate', - component: AdminActivateComponent, - }, - { - path: 'superAdminActivate', - component: SuperAdminActivateComponent, + component: AdminActivateComponent }, - { + { path: 'category-views', component: CategoryTableComponent, canActivate: [TokenAdminGuard], }, - { - path: 'video-views', + { + path: 'video-views', component: VideoViewsComponent, canActivate: [TokenAdminGuard], }, @@ -124,14 +119,14 @@ const routes: Routes = [ canActivate: [TokenAdminGuard], }, { - path: 'record', - component: RecordComponent, - canActivate: [AuthGuard], + path: 'record', + component: RecordComponent, + canActivate: [AuthGuard] }, { - path: 'controleSuperAdmin', - component: ControleSuperAdminComponent, - canActivate: [TokenSuperAdminGuard], + path: 'notifications', + component: NotificationsComponent, + canActivate: [AuthGuard] }, ]; @@ -139,4 +134,4 @@ const routes: Routes = [ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) -export class AppRoutingModule {} +export class AppRoutingModule { } \ No newline at end of file diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts old mode 100755 new mode 100644 index 4f69703d..300bf87e --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; // Import necessário import { AppComponent } from './app.component'; import { BackgroundComponent } from './components/background/background.component'; import { ToastModule } from 'primeng/toast'; @@ -7,14 +8,19 @@ import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; import { MenuModule } from 'primeng/menu'; - describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule, ToastModule, ConfirmDialogModule, MenuModule], + imports: [ + RouterTestingModule, + HttpClientTestingModule, + ToastModule, + ConfirmDialogModule, + MenuModule + ], declarations: [AppComponent, BackgroundComponent], providers: [MessageService, ConfirmationService], }).compileComponents(); diff --git a/src/app/app.module.ts b/src/app/app.module.ts old mode 100755 new mode 100644 index e77d0785..b031584d --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -14,8 +14,8 @@ import { ButtonModule } from 'primeng/button'; import { SocialLoginModule, SocialAuthServiceConfig, GoogleLoginProvider, FacebookLoginProvider } from '@abacritt/angularx-social-login'; // Declaration +import {CommonModule} from '@angular/common'; import { NgModule, isDevMode } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { AppComponent } from './app.component'; import { LoginComponent } from './pages/login/login.component'; import { RegisterComponent } from './pages/register/register.component'; @@ -47,13 +47,12 @@ import { PrivacyPolicyComponent } from './pages/privacy-policy/privacy-policy.co import { ServiceWorkerModule } from '@angular/service-worker'; import { NgChartsModule } from 'ng2-charts'; import { HomeAdminComponent } from './pages/home-admin/home-admin.component'; -import {ControleSuperAdminComponent} from './pages/controle-super-admin/controle-super-admin.component' - import { CategoryTableComponent } from './pages/category-table/category-table.component'; import { VideoViewsComponent } from './pages/video-views/video-views.component'; import { DashboardCategoryComponent } from './pages/dashboard-category/dashboard-category.component'; import { RecordComponent } from './pages/record/record.component'; +import { NotificationsComponent } from './pages/notifications/notifications.component'; @NgModule({ imports: [ @@ -112,7 +111,7 @@ import { RecordComponent } from './pages/record/record.component'; VideoViewsComponent, DashboardCategoryComponent, RecordComponent, - ControleSuperAdminComponent, + NotificationsComponent, ], providers: [ @@ -150,4 +149,4 @@ import { RecordComponent } from './pages/record/record.component'; ], bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule { } \ No newline at end of file diff --git a/src/app/components/background/background.component.css b/src/app/components/background/background.component.css old mode 100755 new mode 100644 index 10df2096..b49be73b --- a/src/app/components/background/background.component.css +++ b/src/app/components/background/background.component.css @@ -9,3 +9,25 @@ border: none; box-shadow: none; } + +:host ::ng-deep .notification-badge-wrapper { + pointer-events: none; + position: relative; + top: -1em; + left: -1.8em; + display: flex; + align-items: center; + justify-content: center; +} + +:host ::ng-deep .notification-badge { + background-color: red; + color: white; + border-radius: 50%; + padding: 0.7em 0.5em; + font-size: 0.7em; + margin-left: 0.5em; + display: inline-block; + min-width: 1.5em; + text-align: center; +} diff --git a/src/app/components/background/background.component.html b/src/app/components/background/background.component.html old mode 100755 new mode 100644 index ad988b47..16b99603 --- a/src/app/components/background/background.component.html +++ b/src/app/components/background/background.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/components/background/background.component.spec.ts b/src/app/components/background/background.component.spec.ts old mode 100755 new mode 100644 index a9058ab1..fd19a8d2 --- a/src/app/components/background/background.component.spec.ts +++ b/src/app/components/background/background.component.spec.ts @@ -1,118 +1,95 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { BackgroundComponent } from './background.component'; import { RouterTestingModule } from '@angular/router/testing'; import { MenuModule } from 'primeng/menu'; +import { NotificationService } from 'src/app/services/notification.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; describe('BackgroundComponent', () => { let component: BackgroundComponent; let fixture: ComponentFixture; + let notificationService: NotificationService; beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [RouterTestingModule, MenuModule, HttpClientTestingModule], declarations: [BackgroundComponent], - imports: [RouterTestingModule, MenuModule], + providers: [ + { + provide: NotificationService, + useValue: { + fetchRecommendedVideosCount: jasmine.createSpy('fetchRecommendedVideosCount').and.returnValue(of({ recommend_videos: [{}, {}, {}] })), + recommendedVideosCount$: of(5), + isAuthenticated: jasmine.createSpy('isAuthenticated').and.returnValue(true), + setUserIdFromToken: jasmine.createSpy('setUserIdFromToken'), + userId: 'mockUserId', + updateRecommendedVideosCount: jasmine.createSpy('updateRecommendedVideosCount') + } + } + ], }).compileComponents(); fixture = TestBed.createComponent(BackgroundComponent); component = fixture.componentInstance; + notificationService = TestBed.inject(NotificationService); + fixture.detectChanges(); }); + afterEach(() => { + component.ngOnDestroy(); // Certifique-se de limpar após cada teste + }); + it('should create', () => { expect(component).toBeTruthy(); }); - describe('Identifies User Device', () => { - it('should identify a mobile device - Android', () => { - // mock userAgent for Android - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (Linux; Android 10; Pixel 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); - }); + it('should initialize items and start fetching notifications', fakeAsync(() => { + expect(notificationService.fetchRecommendedVideosCount).toHaveBeenCalled(); + expect(notificationService.setUserIdFromToken).toHaveBeenCalled(); + expect(component.hasNotifications).toBeTrue(); // Verifica se as notificações foram atualizadas + })); - it('should identify a mobile device - iPhone', () => { - // mock userAgent for iPhone - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.3 Mobile/15E148 Safari/604.1', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); - }); + it('should correctly update the notification count', fakeAsync(() => { + const response = { recommend_videos: [{}, {}, {}] }; + component.updateNotificationCount(response); - it('should identify a mobile device - iPad', () => { - // mock userAgent for iPad - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (iPad; CPU OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); - }); + tick(); // Avança o tempo para garantir a execução da lógica - it('should identify a mobile device - iPod', () => { - // mock userAgent for iPod - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (iPod; CPU iPhone OS 14_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); - }); + expect(notificationService.updateRecommendedVideosCount).toHaveBeenCalledWith(3); + expect(component.hasNotifications).toBeTrue(); + })); - it('should identify a mobile device - Windows Phone', () => { - // mock userAgent for Windows Phone - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36 Edge/13.10586', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); - }); + it('should correctly identify the user device as mobile or not', () => { + const originalUserAgent = navigator.userAgent; + const mobileUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'; - it('should identify a mobile device - BlackBerry', () => { - // mock userAgent for BlackBerry - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeTruthy(); + Object.defineProperty(navigator, 'userAgent', { + value: mobileUserAgent, + writable: true, + configurable: true }); - it('should identify a non-mobile device', () => { - // mock userAgent - Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0 (Edition std-1)', - configurable: true, - writable: true, - }); - - component.identifiesUserDevice(); - expect(component.mobileDevide).toBeFalsy(); + component.identifiesUserDevice(); + expect(component.mobileDevide).toBeTrue(); // Agora deve passar + + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent }); }); + + it('should update the notification label', () => { + component.updateNotificationLabel(); + fixture.detectChanges(); + + const notificationItem = component.items.find(item => item.routerLink === '/notifications'); + expect(notificationItem?.label).toContain('Notificações { + const unsubscribeSpy = spyOn(component['intervalSubscription'] as any, 'unsubscribe'); + component.ngOnDestroy(); + expect(unsubscribeSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/components/background/background.component.ts b/src/app/components/background/background.component.ts old mode 100755 new mode 100644 index 8367720a..423284a6 --- a/src/app/components/background/background.component.ts +++ b/src/app/components/background/background.component.ts @@ -1,5 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { MenuItem, MessageService } from 'primeng/api'; +import { NotificationService } from 'src/app/services/notification.service'; +import { Subscription, interval } from 'rxjs'; @Component({ selector: 'app-background', @@ -7,13 +9,20 @@ import { MenuItem, MessageService } from 'primeng/api'; styleUrls: ['./background.component.css'], providers: [MessageService], }) -export class BackgroundComponent implements OnInit { +export class BackgroundComponent implements OnInit, OnDestroy { items: MenuItem[] = []; mobileDevide: boolean = true; + hasNotifications: boolean = false; // Indica se há notificações + private intervalSubscription: Subscription | null = null; - constructor() {} + constructor( + private notificationService: NotificationService, + private cdr: ChangeDetectorRef + ) {} ngOnInit(): void { + console.log('BackgroundComponent initialized'); + this.items = [ { label: 'Perfil', @@ -22,26 +31,93 @@ export class BackgroundComponent implements OnInit { { label: 'Histórico de Vídeos', routerLink: '/record', + }, + { + label: `Notificações`, + routerLink: '/notifications', + escape: false, } ]; + + // Atualiza as notificações imediatamente com base no serviço + this.notificationService.recommendedVideosCount$.subscribe(count => { + this.hasNotifications = count > 0; // Verifica se há notificações + this.updateNotificationLabel(); + }); + + if (this.notificationService.isAuthenticated()) { + console.log('User is authenticated'); + const token = localStorage.getItem('token') as string; + this.notificationService.setUserIdFromToken(token); + const userId = this.notificationService.userId; + this.notificationService.fetchRecommendedVideosCount(userId) + .subscribe(response => { + console.log('Response from fetchRecommendedVideosCount:', response); + this.updateNotificationCount(response); + }); + } else { + console.log('User is not authenticated'); + } + + this.intervalSubscription = interval(300000).subscribe(() => { + if (this.notificationService.isAuthenticated()) { + const token = localStorage.getItem('token') as string; + this.notificationService.setUserIdFromToken(token); + const userId = this.notificationService.userId; + this.notificationService.fetchRecommendedVideosCount(userId) + .subscribe(response => { + console.log('Response from interval fetch:', response); + this.updateNotificationCount(response); + }); + } + + this.notificationService.recommendedVideosCount$.subscribe(count => { + console.log('New notifications count (from BehaviorSubject):', count); + this.hasNotifications = count > 0; // Verifica se há notificações + this.updateNotificationLabel(); + }); + }); + this.identifiesUserDevice(); } - identifiesUserDevice(): void { - if ( - RegExp(/Android/i).exec(navigator.userAgent) || - RegExp(/iPhone/i).exec(navigator.userAgent) || - RegExp(/iPad/i).exec(navigator.userAgent) || - RegExp(/iPod/i).exec(navigator.userAgent) || - RegExp(/BlackBerry/i).exec(navigator.userAgent) || - RegExp(/Windows Phone/i).exec(navigator.userAgent) - ) { - this.mobileDevide = true; // está utilizando dispositivo móvel + ngOnDestroy(): void { + if (this.intervalSubscription) { + this.intervalSubscription.unsubscribe(); + } + } + + updateNotificationCount(response: any): void { + if (response && response.recommend_videos) { + const count = response.recommend_videos.length; + console.log('Updating notification count with:', count); + this.hasNotifications = count > 0; + this.notificationService.updateRecommendedVideosCount(count); + this.updateNotificationLabel(); } else { - this.mobileDevide = false; + console.log('No videos found in response'); + this.hasNotifications = false; + this.updateNotificationLabel(); } } + + identifiesUserDevice(): void { + const userAgent = navigator.userAgent; + this.mobileDevide = /Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(userAgent); + } + getActualRoute(): string { return window.location.pathname; } + + updateNotificationLabel(): void { + console.log('Updating notification label'); + this.items = this.items.map(item => { + if (item.routerLink === '/notifications') { + item.label = `Notificações ${this.hasNotifications ? '' : ''}`; + } + return item; + }); + this.cdr.detectChanges(); // Certifique-se de que a interface seja atualizada + } } diff --git a/src/app/pages/notifications/notifications.component.css b/src/app/pages/notifications/notifications.component.css new file mode 100644 index 00000000..c520ddf7 --- /dev/null +++ b/src/app/pages/notifications/notifications.component.css @@ -0,0 +1,69 @@ +.videos-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 75em; + margin: 0 auto; + padding: 0 1.25em; +} + +.header-title { + color: green; + text-align: center; + margin-bottom: 1.25em; + font-size: 2em; + font-weight: bold; + text-transform: uppercase; +} + +.button-container { + text-align: center; + margin-bottom: 1.25em; +} + +.mark-read-button, .btn-read { + background-color: #2980b9; + color: #fff; + border: none; + padding: 0.75em 1.5em; + margin-bottom: 1.25em; + border-radius: 0.3125em; + cursor: pointer; + font-size: 1.1em; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.mark-read-button:hover, .btn-read:hover { + background-color: #1f6395; +} + +.videos { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + max-width: 75em; +} + +.flex { + display: flex; + flex-direction: column; + align-items: center; + transition: transform 0.3s ease; +} + +.video-img { + display: block; + margin: 0 auto; + border-radius: 7px; + width: 220px; + height: 124px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s ease, transform 0.3s ease; +} + +.video-img:hover { + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2); + transform: translateY(-5px); +} diff --git a/src/app/pages/notifications/notifications.component.html b/src/app/pages/notifications/notifications.component.html new file mode 100644 index 00000000..bc8f6688 --- /dev/null +++ b/src/app/pages/notifications/notifications.component.html @@ -0,0 +1,13 @@ +
+

Notificações

+
+ +
+
+
+
+ +
+
+
+
diff --git a/src/app/pages/notifications/notifications.component.spec.ts b/src/app/pages/notifications/notifications.component.spec.ts new file mode 100644 index 00000000..ab047759 --- /dev/null +++ b/src/app/pages/notifications/notifications.component.spec.ts @@ -0,0 +1,106 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NotificationsComponent } from './notifications.component'; +import { VideoService } from 'src/app/services/video.service'; +import { IVideo } from 'src/shared/model/video.model'; +import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { NotificationService } from 'src/app/services/notification.service'; +import { AuthService } from 'src/app/services/auth.service'; + +describe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + let videoService: VideoService; + let notificationService: NotificationService; + let authService: AuthService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [NotificationsComponent], + providers: [VideoService, NotificationService, AuthService], + }).compileComponents(); + + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + videoService = TestBed.inject(VideoService); + notificationService = TestBed.inject(NotificationService); + authService = TestBed.inject(AuthService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should populate recommendedVideos on fetchRecommendedVideos success', async () => { + component.unbTvVideos = [ + { id: 1, title: 'Video 1', channels: [{ id: 1, name: 'unbtv' }] }, + { id: 2, title: 'Video 2', channels: [{ id: 1, name: 'unbtv' }] }, + { id: 3, title: 'Video 3', channels: [{ id: 1, name: 'unbtv' }] } + ]; + + const recommendedVideoIds = [1, 2]; + + spyOn(authService, 'isAuthenticated').and.returnValue(true); + spyOn(notificationService, 'fetchRecommendedVideosCount').and.returnValue(of({ recommend_videos: recommendedVideoIds })); + spyOn(notificationService, 'setUserIdFromToken'); + + await component.fetchRecommendedVideos(); + + expect(component.recommendedVideos.length).toBe(2); + expect(component.recommendedVideos.map(v => v.id)).toEqual([1, 2]); + }); + + it('should filter videos by channel and populate unbTvVideos', () => { + const mockVideos: IVideo[] = [ + { id: 1, title: 'Video 1', channels: [{ id: 12, name: "unbtvchannel" }] }, + { id: 2, title: 'Video 2', channels: [{ id: 13, name: "otherchannel" }] } + ]; + + component.unbTvChannelId = 12; + component.unbTvVideos = []; + + component.filterVideosByChannel(mockVideos); + + expect(component.unbTvVideos.length).toBe(1); + expect(component.unbTvVideos[0].id).toBe(1); + }); + + it('should call findAll service method and set videosEduplay', async () => { + const expectedData = { + body: { + videoList: [{ id: 1, title: 'Eduplay Video 1' }] + } + }; + + const findAllSpy = spyOn(videoService, 'findAll').and.returnValue(of(new HttpResponse({ body: expectedData.body }))); + const filterSpy = spyOn(component, 'filterVideosByChannel').and.callThrough(); + const videosCatalogSpy = spyOn(videoService, 'videosCatalog').and.callThrough(); + + await component.findAll(); + + expect(findAllSpy).toHaveBeenCalled(); + expect(component.videosEduplay).toEqual(expectedData.body.videoList); + expect(filterSpy).toHaveBeenCalledWith(expectedData.body.videoList); + expect(videosCatalogSpy).toHaveBeenCalledWith(component.unbTvVideos, component.catalog); + }); + + + it('should mark notifications as read', () => { + spyOn(localStorage, 'setItem'); + spyOn(notificationService, 'updateRecommendedVideosCount'); + spyOn(component['cdr'], 'detectChanges'); + + component.markAsRead(); + + expect(localStorage.setItem).toHaveBeenCalledWith('notificationsRead', 'true'); + expect(notificationService.updateRecommendedVideosCount).toHaveBeenCalledWith(0); + expect(component.notificationsRead).toBeTrue(); + expect(component.recommendedVideos.length).toBe(0); + expect(component.numberOfRecommendedVideos).toBe(0); + expect(component['cdr'].detectChanges).toHaveBeenCalled(); + }); +}); + diff --git a/src/app/pages/notifications/notifications.component.ts b/src/app/pages/notifications/notifications.component.ts new file mode 100644 index 00000000..926c87d0 --- /dev/null +++ b/src/app/pages/notifications/notifications.component.ts @@ -0,0 +1,129 @@ +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; +import { VideoService } from 'src/app/services/video.service'; +import { IVideo } from 'src/shared/model/video.model'; +import { AuthService } from 'src/app/services/auth.service'; +import jwt_decode from 'jwt-decode'; +import { UserService } from 'src/app/services/user.service'; +import { UNB_TV_CHANNEL_ID } from 'src/app/app.constant'; +import { Catalog } from 'src/shared/model/catalog.model'; +import { NotificationService } from 'src/app/services/notification.service'; + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.css'] +}) +export class NotificationsComponent implements OnInit { + unbTvVideos: IVideo[] = []; + unbTvChannelId = UNB_TV_CHANNEL_ID; + videosEduplay: IVideo[] = []; + userId: string = ''; + isAuthenticated: boolean = false; + recommendedVideos: IVideo[] = []; + numberOfRecommendedVideos: number = 0; + catalog: Catalog = new Catalog(); + notificationsRead: boolean = false; + + constructor( + private videoService: VideoService, + private authService: AuthService, + private userService: UserService, + private notificationService: NotificationService, + private cdr: ChangeDetectorRef + ) {} + + async ngOnInit(): Promise { + this.isAuthenticated = this.authService.isAuthenticated(); + this.notificationsRead = localStorage.getItem('notificationsRead') === 'true'; + + if (this.isAuthenticated) { + this.setUserIdFromToken(localStorage.getItem('token') as string); + await this.findAll(); + await this.fetchRecommendedVideos(); + this.notificationService.fetchRecommendedVideosCount(this.userId); + } + } + + setUserIdFromToken(token: string) { + const decodedToken: any = jwt_decode(token); + this.userId = decodedToken.id; + } + + fetchRecommendedVideos(): void { + if (this.isAuthenticated) { + const token = localStorage.getItem('token') as string; + this.notificationService.setUserIdFromToken(token); + + this.notificationService.fetchRecommendedVideosCount(this.notificationService.userId) + .subscribe({ + next: (response) => { + if (response && Array.isArray(response.recommend_videos)) { + const recommended_video_ids = response.recommend_videos.map((id: number) => String(id)); + + this.recommendedVideos = this.unbTvVideos.filter(video => recommended_video_ids.includes(String(video.id))); + } else { + console.warn('A estrutura da resposta da API não está conforme o esperado:', response); + } + + console.log('Vídeos recomendados recebidos:', this.recommendedVideos); + console.log(this.recommendedVideos[0]); + this.numberOfRecommendedVideos = this.recommendedVideos.length; + }, + error: (error) => { + console.log('Erro ao buscar vídeos recomendados', error); + } + }); + } + } + + markAsRead(): void { + // Zera as notificações e atualiza a interface + localStorage.setItem('notificationsRead', 'true'); + this.notificationsRead = true; + this.recommendedVideos = []; + this.numberOfRecommendedVideos = 0; + + // Atualiza o contador de notificações no serviço para 0 + this.notificationService.updateRecommendedVideosCount(0); + + // Força a atualização da interface + this.cdr.detectChanges(); + } + + findAll(): Promise { + return new Promise((resolve, reject) => { + this.videoService.findAll().subscribe({ + next: (data) => { + this.videosEduplay = data.body?.videoList ?? []; + }, + error: (error) => { + console.log(error); + reject(error); + }, + complete: () => { + this.filterVideosByChannel(this.videosEduplay); + this.videoService.videosCatalog(this.unbTvVideos, this.catalog); + + // Verificar o conteúdo dos vídeos carregados + console.log('Vídeos do canal UNB TV carregados:', this.unbTvVideos); + + resolve(); + }, + }); + }); + } + + filterVideosByChannel(videos: IVideo[]): void { + videos.forEach((video) => { + const channel = video?.channels; + + if (channel && channel[0].id === this.unbTvChannelId) { + this.unbTvVideos.push(video); + } + }); + } + + trackByVideoId(index: number, video: IVideo): string { + return video.id ? video.id.toString() : index.toString(); + } +} diff --git a/src/app/services/notification.service.spec.ts b/src/app/services/notification.service.spec.ts new file mode 100644 index 00000000..f85bdba4 --- /dev/null +++ b/src/app/services/notification.service.spec.ts @@ -0,0 +1,78 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { NotificationService } from './notification.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { IVideo } from 'src/shared/model/video.model'; +import { of } from 'rxjs'; + +describe('NotificationService', () => { + let service: NotificationService; + let httpMock: HttpTestingController; + let authServiceSpy: jasmine.SpyObj; + + beforeEach(() => { + const authSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + NotificationService, + { provide: AuthService, useValue: authSpy }, + ], + }); + + service = TestBed.inject(NotificationService); + authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj; + httpMock = TestBed.inject(HttpTestingController); + + // Mocka setUserIdFromToken para evitar o erro de token inválido + spyOn(service, 'setUserIdFromToken').and.callFake(() => { + service.userId = '12345'; // Define um userId válido + }); + }); + + afterEach(() => { + httpMock.verify(); // Verifica se não há solicitações pendentes + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch and update recommended videos count', () => { + const mockRecommendedVideos: IVideo[] = [ + { id: 1, title: 'Video 1' }, + { id: 2, title: 'Video 2' } + ]; + + authServiceSpy.isAuthenticated.and.returnValue(true); + service.setUserIdFromToken('fakeToken'); + + service.fetchRecommendedVideosCount('12345').subscribe(response => { + expect(response.recommend_videos.length).toBe(2); + }); + + const req = httpMock.expectOne(`${service['videoServiceApiURL']}/recommendation/get_recommendation_record/?user_id=12345`); + expect(req.request.method).toBe('GET'); + req.flush({ recommend_videos: mockRecommendedVideos }); + + service.updateRecommendedVideosCount(2); + service.recommendedVideosCount$.subscribe(count => { + expect(count).toBe(2); + }); + }); + + it('should not make HTTP request if not authenticated', () => { + authServiceSpy.isAuthenticated.and.returnValue(false); + + // Aqui vamos mockar a função fetchRecommendedVideosCount para garantir que ela retorne sem fazer a requisição + spyOn(service, 'fetchRecommendedVideosCount').and.callFake(() => of({ recommend_videos: [] })); + + service.fetchRecommendedVideosCount('12345').subscribe(response => { + expect(response.recommend_videos).toEqual([]); + }); + + // Verifica se realmente não houve nenhuma requisição HTTP + httpMock.expectNone(`${service['videoServiceApiURL']}/recommendation/get_recommendation_record/?user_id=12345`); + }); +}); diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts new file mode 100644 index 00000000..3d12b62d --- /dev/null +++ b/src/app/services/notification.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { AuthService } from 'src/app/services/auth.service'; +import { IVideo } from 'src/shared/model/video.model'; +import jwt_decode from 'jwt-decode'; +import { environment } from '../environment/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationService { + public recommendedVideosCountSource = new BehaviorSubject(0); + public recommendedVideosCount$ = this.recommendedVideosCountSource.asObservable(); + public userId: string = ''; + public recommendedVideos: IVideo[] = []; + private videoServiceApiURL = environment.videoAPIURL; // URL base correta + + constructor( + private http: HttpClient, + private authService: AuthService + ) {} + + updateRecommendedVideosCount(count: number) { + this.recommendedVideosCountSource.next(count); + } + + isAuthenticated(): boolean { + return this.authService.isAuthenticated(); + } + + setUserIdFromToken(token: string) { + const decodedToken: any = jwt_decode(token); + this.userId = decodedToken.id; + } + + fetchRecommendedVideosCount(userId: string): Observable { + return this.http.get<{ recommend_videos: IVideo[] }>(`${this.videoServiceApiURL}/recommendation/get_recommendation_record/`, { + params: { user_id: userId } + }); + } +} + diff --git a/test-reports/TESTS.xml b/test-reports/TESTS.xml index 7e62c569..59c7f045 100644 --- a/test-reports/TESTS.xml +++ b/test-reports/TESTS.xml @@ -525,4 +525,4 @@ Error: Erro ]]> - \ No newline at end of file +