Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6a2ba64
feat(signup): Angular version of signup page using existing backend.
gtandersen Apr 28, 2026
52e593f
fix(signup): Add missing license header.
gtandersen May 5, 2026
2c620cd
fix(signup): Correct signup file license headers for policy test
gtandersen May 5, 2026
8fb2053
fix(test): Attempt to fix failing test for AliasesListerComponent.
gtandersen May 5, 2026
b260bd3
fix(test): Attempt to fix failing tests for AliasesEditorModalComponent.
gtandersen May 5, 2026
647c468
fix(test): Attempt to fix failing tests for HelpComponent.
gtandersen May 5, 2026
9640690
fix(test): Fix CI unit tests by aligning Angular and preferences mocks
gtandersen May 5, 2026
91ab210
fix(test): Fix missing Angular test imports in WelcomeDesk spec.
gtandersen May 6, 2026
54dd4fc
test(signup): Stabilize signup custom domain validation test.
gtandersen May 6, 2026
c487113
fix(test): Silence expected websocket error log in unit test.
gtandersen May 6, 2026
d85b71e
fix(test): Add Material list module to SingleMailViewer spec.
gtandersen May 6, 2026
9966764
fix(test): Fix flaky preferences uid test by replaying mocked uid.
gtandersen May 6, 2026
876a068
fix(test): Remove fakeAsync from aliases dialog default email test.
gtandersen May 6, 2026
5b84de2
fix(test): Add MatIconModule to WelcomeDesk spec.
gtandersen May 6, 2026
0ef8959
fix(test): Stabilize aliases allowed domains dialog test.
gtandersen May 6, 2026
4dfd24d
test(signup): Use real form submit in signup validation test.
gtandersen May 6, 2026
fd58d70
test(signup): Split signup Cypress tests into CI and deployed variants.
gtandersen May 6, 2026
81adf84
test(signup): Use local signup route in CI Cypress spec.
gtandersen May 6, 2026
e335dbf
fix(signup): Fix signup route fallback and update legacy Material Sas…
gtandersen May 22, 2026
7745e98
fix(signup): Refine signup tooltip triggers and popover positioning.
gtandersen May 22, 2026
de8f063
fix(signup): Add password visibility toggle to signup form.
gtandersen May 30, 2026
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
100 changes: 100 additions & 0 deletions e2e/cypress/integration/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// --------- 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 ----------

/// <reference types="cypress" />

describe('Signup', () => {
beforeEach(() => {
cy.intercept('GET', '/signup?legacy=1&runbox7=1', {
statusCode: 200,
body: `
<html>
<body>
<form name="signup" action="/mail/signup">
<select name="runboxDomain">
<option value="runbox.com">runbox.com</option>
<option value="runbox.no">runbox.no</option>
<option value="rbx.email">rbx.email</option>
</select>
<div class="h-captcha" data-sitekey="test-site-key"></div>
</form>
</body>
</html>
`,
headers: {
'content-type': 'text/html',
},
}).as('legacySignup');

cy.intercept('GET', 'https://hcaptcha.com/1/api.js?render=explicit', {
statusCode: 200,
body: 'window.hcaptcha = { render: function() { return "test-widget"; } };',
headers: {
'content-type': 'application/javascript',
},
}).as('hcaptchaScript');
});

it('should render the Angular signup page in the local mock-backed environment', () => {
cy.visit('/signup?runbox7=1');
cy.wait('@legacySignup');
cy.wait('@hcaptchaScript');

cy.location('pathname').should('eq', '/signup');
cy.location('search').should('contain', 'runbox7=1');
cy.contains('h1', 'Create a Runbox Account').should('exist');
cy.get('form.signup-form').should('have.attr', 'action', '/mail/signup');
cy.get('input[name="user"]').should('exist');
cy.get('input[name="first_name"]').should('exist');
cy.get('input[name="last_name"]').should('exist');
cy.get('input[name="password"]').should('exist');
cy.get('select[name="runboxDomain"]').find('option').should('have.length', 3);
cy.get('select[name="runboxDomain"]').find('option').then((options) => {
const domains = Array.from(options).map((option) => option.value);
expect(domains).to.include('runbox.com');
expect(domains).to.include('rbx.email');
});
cy.get('div.captcha-host').should('exist');
cy.contains('button.submit', 'Set up my Runbox account').should('exist');
});

it('should show the public trust and transparency content', () => {
cy.visit('/signup?runbox7=1');
cy.wait('@legacySignup');
cy.wait('@hcaptchaScript');

cy.get('header.signup-header .brand img').should('be.visible');
cy.get('header.signup-header .brand').should('not.contain', 'Runbox 7');

cy.contains('.hero-panel h2', 'Privacy by business model').should('exist');
cy.contains('.hero-panel h2', 'Hosted in Norway').should('exist');
cy.contains('.hero-panel h2', 'Sustainable and secure').should('exist');
cy.contains('.hero-panel h2', 'How the trial works').should('exist');

cy.contains('.hero-panel', 'customer email content is private').should('exist');
cy.contains('.form-section', 'default sender name recipients will see').should('exist');

cy.get('.info-chip').should('have.length.at.least', 3);
cy.contains('.field-label, .field small', 'Existing email address').should('exist');
cy.contains('.field-label, .field small', 'How did you hear about Runbox?').should('exist');

cy.contains('.form-actions button.submit', 'Set up my Runbox account').should('exist');
cy.contains('a', 'Use legacy signup page').should('not.exist');
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build": "node src/build/pre-build.js && ng build --configuration production --base-href=/app/ runbox7; RES=$?; node src/build/post-build.js; exit $RES",
"policy": "node policy-tests/run-all.js",
"test": "ng test",
"test:signup:firefox": "ng test --watch=false --browsers Firefox --include src/app/signup/signup.component.spec.ts",
"lint": "ng lint",
"e2e": "ng e2e",
"start-e2e-server": "start-test mockserver 15000 start-use-mockserver",
Expand Down
35 changes: 24 additions & 11 deletions src/app/aliases/aliases.lister.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
// along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
// ---------- END RUNBOX LICENSE ----------

import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { AliasesListerComponent } from './aliases.lister';
import { MatLegacyDialogModule } from '@angular/material/legacy-dialog';
import { MatLegacyButtonModule } from '@angular/material/legacy-button';
import { MatLegacyCardModule } from '@angular/material/legacy-card';
import { MatLegacyDialog as MatDialog, MatLegacyDialogModule } from '@angular/material/legacy-dialog';
import { MatLegacyInputModule } from '@angular/material/legacy-input';
import { MatLegacySnackBarModule } from '@angular/material/legacy-snack-bar';
import { MatLegacySelectModule } from '@angular/material/legacy-select';
import { MatExpansionModule } from '@angular/material/expansion';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RunboxWebmailAPI } from '../rmmapi/rbwebmail';
import { RMM } from '../rmm';
Expand All @@ -34,6 +40,7 @@ import { HttpClient } from '@angular/common/http';
describe('AliasesListerComponent', () => {
let component: AliasesListerComponent;
let fixture: ComponentFixture<AliasesListerComponent>;
let dialog: MatDialog;

const DEFAULT_EMAIL = 'a.kalou@shadowcat.co.uk';
const ALLOWED_DOMAINS = ['runbox.com', 'shadowcat.co.uk'];
Expand All @@ -47,10 +54,16 @@ describe('AliasesListerComponent', () => {
TestBed.configureTestingModule({
imports: [
CommonModule,
FormsModule,
HttpClientTestingModule,
MatLegacyButtonModule,
MatLegacyCardModule,
MatLegacyCommonModule,
MatLegacyInputModule,
MatLegacySelectModule,
MatLegacySnackBarModule,
MatLegacyDialogModule,
MatExpansionModule,
NoopAnimationsModule,
],
providers: [
Expand Down Expand Up @@ -94,6 +107,7 @@ describe('AliasesListerComponent', () => {
});
fixture = TestBed.createComponent(AliasesListerComponent);
component = fixture.componentInstance;
dialog = TestBed.inject(MatDialog);
});

it('loads aliases through RMM', () => {
Expand Down Expand Up @@ -126,7 +140,7 @@ describe('AliasesListerComponent', () => {
expect(forwards.length).toBe(component.aliases.length, 'all forwards should be shown');
});

it('sets the default email to the current users email', fakeAsync(() => {
it('sets the default email to the current users email', () => {
expect(component.defaultEmail).toBe(DEFAULT_EMAIL);

// spawn a modal
Expand All @@ -143,22 +157,21 @@ describe('AliasesListerComponent', () => {
// FIXME: doesn't work, value isn't set, probably because of ngModel
// expect(forwardTo.value)
// .toBe(DEFAULT_EMAIL, "Forward to should default to the user's email");
}));
});

it('dialog loads allowed domains', () => {
it('dialog loads allowed domains', async () => {
// spawn a modal
component.create();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();

const modal =
fixture.nativeElement.nextSibling.querySelector('app-aliases-edit');
expect(modal).toBeTruthy();

const domain: HTMLSelectElement =
modal.querySelector('mat-select[name=\'domain\']');

ALLOWED_DOMAINS.forEach(allowed_domain => {
expect(domain.textContent).toContain(allowed_domain);
});
const dialogRef = dialog.openDialogs[0];
expect(dialogRef).toBeTruthy();
expect(dialogRef.componentInstance.allowedDomains).toEqual(ALLOWED_DOMAINS);
});
});
5 changes: 3 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ window.addEventListener('dragover', (event) => event.preventDefault());
window.addEventListener('drop', (event) => event.preventDefault());

const routes: Routes = [
{ path: 'signup', loadChildren: () => import('./signup/signup.module').then(m => m.SignupModule) },
{
path: '',
canActivateChild: [RMMAuthGuardService],
Expand Down Expand Up @@ -136,7 +137,8 @@ const routes: Routes = [
{ path: 'domainregistration', component: DomainRegisterRedirectComponent },
]
},
{ path: 'login', component: LoginComponent }
{ path: 'login', component: LoginComponent },
{ path: '**', redirectTo: '' }
];

@NgModule({
Expand Down Expand Up @@ -213,4 +215,3 @@ export class AppModule {
);
}
}

10 changes: 5 additions & 5 deletions src/app/common/preferences.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

import { PreferencesService, DefaultPrefGroups, PreferencesResult } from './preferences.service';
import { ScreenSize } from '../mobile-query.service';
import { of, Subject, firstValueFrom, Observable } from 'rxjs';
import { of, Subject, ReplaySubject, firstValueFrom, Observable } from 'rxjs';
import { take } from 'rxjs/operators';

class MockStorageService {
private store = new Map<string, any>();
me: Observable<any> = of({ uid: 42 });
uid: Observable<number> = of(42);

get(key: string): Promise<any> {
return Promise.resolve(this.store.get(key));
Expand Down Expand Up @@ -241,9 +241,9 @@ describe('PreferencesService', () => {

describe('Integration with StorageService uid AsyncSubject', () => {
it('should handle operations that depend on uid being loaded', async () => {
const uidSubject = new Subject<{ uid: number }>();
const uidSubject = new ReplaySubject<number>(1);
const uidStorage = {
me: uidSubject.asObservable(),
uid: uidSubject.asObservable(),
get: (key: string) => mockStorage.get(key),
set: (key: string, value: any) => mockStorage.set(key, value)
} as any;
Expand All @@ -254,7 +254,7 @@ describe('PreferencesService', () => {
testService.set(DefaultPrefGroups.Desktop, 'uidTestKey', 'uidTestValue');

// Now emit uid
uidSubject.next({ uid: 999 });
uidSubject.next(999);
uidSubject.complete();

await new Promise(resolve => setTimeout(resolve, 50));
Expand Down
4 changes: 2 additions & 2 deletions src/app/compose/compose.component.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import '../../../node_modules/@angular/material/theming';
@use '@angular/material' as mat;

$rmm-default-accent: mat-palette($mat-blue, 100, 50, 200);
$rmm-default-accent: mat.define-palette(mat.$blue-palette, 100, 50, 200);

#messageTextArea {
box-sizing: border-box;
Expand Down
4 changes: 3 additions & 1 deletion src/app/help/help.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// ---------- END RUNBOX LICENSE ----------

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

import { HelpComponent } from './help.component';

Expand All @@ -27,7 +28,8 @@ describe('HelpComponent', () => {

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HelpComponent ]
declarations: [ HelpComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents();
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h4 id="loginHeaderSubTitle">The fastest webmail app on the planet</h4>
<div id="loginArea">
<div>
<img src="assets/runbox7_blue_dark.png" id="logoLogin" alt="Runbox 7" />
<p>Log in below or <a href="/signup?runbox7=1" class="login-link">create a new account</a>.</p>
<p>Log in below or <a routerLink="/signup" [queryParams]="{ runbox7: 1 }" class="login-link">create a new account</a>.</p>
</div>
<mat-form-field>
<input matInput autocomplete="email" placeholder="Username" name="user" type="email" inputmode="email" autofocus ngModel required />
Expand Down
2 changes: 2 additions & 0 deletions src/app/mailviewer/singlemailviewer.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIcon, MatIconModule } from '@angular/material/icon';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatLegacyListModule as MatListModule } from '@angular/material/legacy-list';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
import { MatToolbarModule } from '@angular/material/toolbar';
Expand Down Expand Up @@ -105,6 +106,7 @@ describe('SingleMailViewerComponent', () => {
ResizerModule,
MatIconModule,
MatIconTestingModule,
MatListModule,
MatGridListModule,
MatToolbarModule,
MatTooltipModule,
Expand Down
Loading
Loading