diff --git a/src/app/menu/headertoolbar.component.html b/src/app/menu/headertoolbar.component.html index 72e1f6395..b07681caa 100644 --- a/src/app/menu/headertoolbar.component.html +++ b/src/app/menu/headertoolbar.component.html @@ -2,8 +2,15 @@ diff --git a/src/app/menu/headertoolbar.component.spec.ts b/src/app/menu/headertoolbar.component.spec.ts new file mode 100644 index 000000000..a5f4d441a --- /dev/null +++ b/src/app/menu/headertoolbar.component.spec.ts @@ -0,0 +1,92 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2026 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 is free software: You can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { of, ReplaySubject } from 'rxjs'; + +import { HeaderToolbarComponent } from './headertoolbar.component'; +import { MenuModule } from './menu.module'; +import { LogoutService } from '../login/logout.service'; +import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { RMMOfflineService } from '../rmmapi/rmmoffline.service'; +import { FolderMessageCountMap, MessageListService } from '../rmmapi/messagelist.service'; + +class MockRunboxWebmailAPI { + me = of({ + is_trial: false, + owner: null, + } as RunboxMe); +} + +class MockRMMOfflineService { + is_offline = false; +} + +class MockLogoutService { + logout() {} +} + +class MockMessageListService { + folderMessageCountSubject = new ReplaySubject(1); +} + +describe('HeaderToolbarComponent', () => { + let fixture: ComponentFixture; + let messageListService: MockMessageListService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MenuModule, + MatIconTestingModule, + NoopAnimationsModule, + RouterTestingModule, + ], + providers: [ + { provide: RunboxWebmailAPI, useClass: MockRunboxWebmailAPI }, + { provide: RMMOfflineService, useClass: MockRMMOfflineService }, + { provide: LogoutService, useClass: MockLogoutService }, + { provide: MessageListService, useClass: MockMessageListService }, + ] + }).compileComponents(); + + messageListService = TestBed.inject(MessageListService) as unknown as MockMessageListService; + }); + + it('shows the unread mail count on the Mail menu item', () => { + fixture = TestBed.createComponent(HeaderToolbarComponent); + + messageListService.folderMessageCountSubject.next({ + Inbox: { unread: 2, total: 12 }, + 'Lists.News': { unread: 5, total: 20 }, + Spam: { unread: 0, total: 4 }, + }); + fixture.detectChanges(); + + const mailLink = fixture.debugElement.query(By.css('a[routerLink="/"]')).nativeElement as HTMLElement; + const badge = mailLink.querySelector('.mat-badge-content'); + + expect(badge?.textContent.trim()).toBe('7'); + expect(mailLink.getAttribute('aria-label')).toBe('Mail, 7 unread messages'); + }); +}); diff --git a/src/app/menu/headertoolbar.component.ts b/src/app/menu/headertoolbar.component.ts index ae584a1f3..7abae0ce6 100644 --- a/src/app/menu/headertoolbar.component.ts +++ b/src/app/menu/headertoolbar.component.ts @@ -18,11 +18,11 @@ // ---------- END RUNBOX LICENSE ---------- import { Component, OnInit } from '@angular/core'; -import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { RMMOfflineService } from '../rmmapi/rmmoffline.service'; import { Router } from '@angular/router'; import { LogoutService } from '../login/logout.service'; -import { RunboxMe } from '../rmmapi/rbwebmail'; +import { FolderMessageCountMap, MessageListService } from '../rmmapi/messagelist.service'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -34,16 +34,21 @@ export class HeaderToolbarComponent implements OnInit { rmm6tooltip = 'This area isn\'t upgraded to Runbox 7 yet and will open in a new tab'; user_is_trial = false; isMainAccount: boolean; + mailUnreadCount = 0; constructor( public rmmapi: RunboxWebmailAPI, public rmmoffline: RMMOfflineService, private router: Router, - public logoutservice: LogoutService + public logoutservice: LogoutService, + messagelistservice: MessageListService ) { rmmapi.me.subscribe((me: RunboxMe) => { this.isMainAccount = !me.owner; }); + messagelistservice.folderMessageCountSubject.subscribe(counts => { + this.mailUnreadCount = this.sumUnreadMail(counts); + }); } ngOnInit() { @@ -63,4 +68,24 @@ export class HeaderToolbarComponent implements OnInit { public contacts() { this.router.navigate(['contacts']); } + + get mailMenuAriaLabel(): string { + if (this.mailUnreadCount === 0) { + return 'Mail'; + } + + const messageLabel = this.mailUnreadCount === 1 ? 'message' : 'messages'; + return `Mail, ${this.mailUnreadCount} unread ${messageLabel}`; + } + + private sumUnreadMail(counts: FolderMessageCountMap): number { + if (!counts) { + return 0; + } + + return Object.keys(counts).reduce((total, folder) => { + const unread = counts[folder]?.unread || 0; + return total + (unread > 0 ? unread : 0); + }, 0); + } } diff --git a/src/app/menu/menu.module.ts b/src/app/menu/menu.module.ts index 9967cb900..8efdeccc8 100644 --- a/src/app/menu/menu.module.ts +++ b/src/app/menu/menu.module.ts @@ -19,6 +19,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { MatBadgeModule } from '@angular/material/badge'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatIconModule } from '@angular/material/icon'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; @@ -32,6 +33,7 @@ import { SidenavMenuComponent } from './sidenav-menu.component'; @NgModule({ imports: [ CommonModule, + MatBadgeModule, MatIconModule, MatButtonModule, MatToolbarModule,