Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/app/account-app/account-app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,14 @@ <h3 class="sideNavHeader">Settings Menu</h3>
</nav>

<ng-container toolbar *ngIf="mobileQuery.matches && cart.items | async as items">
<div style="width: 100%; display: flex; justify-content: right">
<div class="mobileSettingsToolbar">
<button mat-icon-button (click)="sidenavService.toggleSidenav()" matTooltip="Toggle settings menu"
id="toggleFolderPaneIcon" aria-label="Toggle settings menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>

<h3 class="sideNavHeader">Settings Menu</h3>

<button mat-mini-fab *ngIf="items.length > 0" routerLink="/account/cart">
<mat-icon [matBadge]="items.length" matBadgePosition="below before" svgIcon="cart">
</mat-icon>
Expand Down
13 changes: 13 additions & 0 deletions src/app/account-app/account-app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,16 @@ mat-expansion-panel-header mat-icon {
mat-expansion-panel-header span {
vertical-align: bottom;
}

.mobileSettingsToolbar {
align-items: center;
display: flex;
gap: 8px;
justify-content: space-between;
width: 100%;
}

.mobileSettingsToolbar .sideNavHeader {
flex: 1;
margin: 0;
}
101 changes: 101 additions & 0 deletions src/app/account-app/account-app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// --------- 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 <https://www.gnu.org/licenses/>.
// ---------- END RUNBOX LICENSE ----------

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatBadgeModule } from '@angular/material/badge';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';

import { AccountAppComponent } from './account-app.component';
import { CartService } from './cart.service';
import { MobileQueryService } from '../mobile-query.service';
import { RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { RunboxSidenavService } from '../runbox-components/runbox-sidenav.service';

@Component({
selector: 'app-runbox-container',
template: '<ng-content select="nav"></ng-content><ng-content select="[toolbar]"></ng-content><ng-content></ng-content>',
})
class RunboxContainerStubComponent {}

describe('AccountAppComponent', () => {
let fixture: ComponentFixture<AccountAppComponent>;
let mobileQuery: MobileQueryService;
let sidenavService: jasmine.SpyObj<RunboxSidenavService>;

beforeEach(async () => {
sidenavService = jasmine.createSpyObj('RunboxSidenavService', ['toggleSidenav']);

await TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
MatBadgeModule,
MatButtonModule,
MatIconModule,
MatIconTestingModule,
MatTooltipModule,
],
declarations: [
AccountAppComponent,
RunboxContainerStubComponent,
],
providers: [
{ provide: CartService, useValue: { items: of([]) } },
{ provide: MobileQueryService, useValue: { matches: true } },
{ provide: RunboxSidenavService, useValue: sidenavService },
{ provide: RunboxWebmailAPI, useValue: { me: of({ owner: null }) } },
],
}).compileComponents();

fixture = TestBed.createComponent(AccountAppComponent);
mobileQuery = TestBed.inject(MobileQueryService);
});

it('shows the mobile settings toggle on mobile screens', () => {
mobileQuery.matches = true;
fixture.detectChanges();

const toggleButton = fixture.nativeElement.querySelector('#toggleFolderPaneIcon');
expect(toggleButton).toBeTruthy();
expect(toggleButton.getAttribute('aria-label')).toBe('Toggle settings menu');
});

it('hides the mobile settings toggle on desktop screens', () => {
mobileQuery.matches = false;
fixture.detectChanges();

const toggleButton = fixture.nativeElement.querySelector('#toggleFolderPaneIcon');
expect(toggleButton).toBeNull();
});

it('toggles the settings navigation when the mobile button is clicked', () => {
mobileQuery.matches = true;
fixture.detectChanges();

fixture.debugElement.query(By.css('#toggleFolderPaneIcon')).nativeElement.click();

expect(sidenavService.toggleSidenav).toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions src/app/account-app/account-app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Component } from '@angular/core';
import { CartService } from './cart.service';
import { MobileQueryService } from '../mobile-query.service';
import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { RunboxSidenavService } from '../runbox-components/runbox-sidenav.service';

@Component({
selector: 'app-account-app-component',
Expand All @@ -34,6 +35,7 @@ export class AccountAppComponent {
constructor(
public cart: CartService,
public mobileQuery: MobileQueryService,
public sidenavService: RunboxSidenavService,
rmmapi: RunboxWebmailAPI,
) {
rmmapi.me.subscribe((me: RunboxMe) => {
Expand Down
3 changes: 0 additions & 3 deletions src/app/account-app/account-upgrades.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<section class="mat-typography">
<div class="appPageHeader">
<button mat-icon-button (click)="sidenavService.toggleSidenav();" matTooltip="Toggle side pane" id="toggleFolderPaneIcon">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<h1> Plans & Upgrades </h1>
</div>

Expand Down
2 changes: 0 additions & 2 deletions src/app/account-app/account-upgrades.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { RMM } from '../rmm';
import { DataUsageInterface } from '../rmm/account-storage';
import { RunboxTimerComponent } from './runbox-timer';
import { AsyncSubject } from 'rxjs';
import { RunboxSidenavService } from '../runbox-components/runbox-sidenav.service';
import { ProductOrder } from './product-order';

import { Decimal } from 'decimal.js-light';
Expand Down Expand Up @@ -74,7 +73,6 @@ export class AccountUpgradesComponent implements OnInit {
public rmmapi: RunboxWebmailAPI,
private snackbar: MatSnackBar,
private rmm: RMM,
public sidenavService: RunboxSidenavService,
private router: Router,
) {
this.router.events.subscribe(e => {
Expand Down
4 changes: 0 additions & 4 deletions src/app/account-app/account-welcome.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
<section class="mat-typography">
<div class="appPageHeader">
<button mat-icon-button (click)="sidenavService.toggleSidenav();" matTooltip="Toggle side pane" id="toggleFolderPaneIcon">
<mat-icon svgIcon="menu"></mat-icon>
</button>

<h1>Account Settings</h1>
</div>

Expand Down
5 changes: 1 addition & 4 deletions src/app/account-app/account-welcome.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import { Component } from '@angular/core';
import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { RunboxSidenavService } from '../runbox-components/runbox-sidenav.service';

@Component({
selector: 'app-account-welcome-component',
Expand All @@ -30,9 +29,7 @@ export class AccountWelcomeComponent {
rmm6tooltip = 'This area isn\'t upgraded to Runbox 7 yet and will open in a new tab';
isMainAccount: boolean;

constructor(rmmapi: RunboxWebmailAPI,
public sidenavService: RunboxSidenavService,
) {
constructor(rmmapi: RunboxWebmailAPI) {
rmmapi.me.subscribe((me: RunboxMe) => {
this.isMainAccount = !me.owner;
});
Expand Down
1 change: 1 addition & 0 deletions src/app/runbox-components/runbox-container.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<ng-content select="[toolbar]"></ng-content>
<ng-content></ng-content>
</mat-sidenav-content>
</mat-sidenav-container>
91 changes: 91 additions & 0 deletions src/app/runbox-components/runbox-container.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// --------- 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 <https://www.gnu.org/licenses/>.
// ---------- END RUNBOX LICENSE ----------

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { Subject } from 'rxjs';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';

import { MobileQueryService } from '../mobile-query.service';
import { RunboxContainerComponent } from './runbox-container';

@Component({
selector: 'app-sidenav-menu',
template: '',
})
class SidenavMenuStubComponent {}

@Component({
template: `
<app-runbox-container>
<nav><div id="projected-nav">Settings nav</div></nav>
<div toolbar id="projected-toolbar">Toolbar</div>
<div id="projected-content">Content</div>
</app-runbox-container>
`,
})
class HostComponent {}

describe('RunboxContainerComponent', () => {
let fixture: ComponentFixture<HostComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MatListModule,
MatSidenavModule,
NoopAnimationsModule,
RouterTestingModule.withRoutes([]),
],
declarations: [
HostComponent,
RunboxContainerComponent,
SidenavMenuStubComponent,
],
providers: [
{ provide: MobileQueryService, useValue: { matches: true, changed: new Subject<boolean>() } },
],
}).compileComponents();

fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
});

it('projects toolbar content into the sidenav content area', () => {
const sidenavContent = fixture.nativeElement.querySelector('mat-sidenav-content');
const toolbar = fixture.nativeElement.querySelector('#projected-toolbar');
const mainContent = fixture.nativeElement.querySelector('#projected-content');

expect(toolbar).toBeTruthy();
expect(mainContent).toBeTruthy();
expect(sidenavContent.textContent).toContain('Toolbar');
expect(sidenavContent.textContent).toContain('Content');
});

it('keeps navigation content inside the sidenav', () => {
const sideMenu = fixture.nativeElement.querySelector('mat-sidenav');
const nav = fixture.nativeElement.querySelector('#projected-nav');

expect(nav).toBeTruthy();
expect(sideMenu.textContent).toContain('Settings nav');
});
});